├── README.md ├── cnb_tvbox_tools.py ├── requirements.txt └── tvbox_tools.py /README.md: -------------------------------------------------------------------------------- 1 | ## [tvbox_tools](https://hub.docker.com/r/2011820123/tvbox) 2 | 写这个工具的主要原因是网上各种接口重复率和失效率极高。几个多仓接口能有成百上千个线路,实际上不重复、可用的线路只有那么几十个,实在是过于冗余了。所以做了这个整理工具,把接口中所有线路进行去重和格式化,json下载保存为同名txt文件,jar文件保存到jar目录下,最后输出个all.json(包含所有历史下载线路接口)和{target}.json(本次下载线路接口,默认tvbox.json)文件用于配置app,看起来比较简洁,方便修改维护。 3 | 4 | ## 功能概述 5 | - 支持多仓、单仓、线路接口的私有化(json和对应的jar文件下载到本地,经过格式化后推送到自己的git仓库) 6 | - 支持js动态渲染数据的接口 7 | - 移除失效线路 8 | - 移除名称中的emoj表情 9 | - 根据hash和文件大小去重线路 10 | - 为文件链接自动使用加速(支持多种加速) 11 | - 默认使用 https://githubfast.com 加速clone、push(push文件过大会限制) 12 | 13 | - 新增`cnb_tvbox_tools.py`,专用于将在线接口私有化到`https://cnb.cool/`,并支持将site中的文件(外挂jar、api中的py或drpy2.min.js、ext中的json)一同私有化。效果参考: 14 | - https://cnb.cool/fish2018/duanju/-/git/raw/main/tvbox.json 15 | - https://cnb.cool/fish2018/test/-/git/raw/main/tvbox.json 16 | 17 | ## 使用方法: 18 | 19 | #### 参数选项 20 | docker run时使用-e选项通过环境变量传参 21 | 22 | - [ * ] username or u 指定用户名 23 | - [ * ] token [github.com中设置token](https://github.com/settings/tokens) 24 | - [ * ] url 指定要下载的源,多个url使用英文逗号分隔,`?&signame=`指定单线路名 25 | - repo 指定你的代码仓库名,默认tvbox 26 | - target 指定你想保存的json文件名,默认tvbox.json 27 | - num 多仓时可以指定下载前num个仓库源 28 | - timeout http请求超时,默认3s 29 | - signame url是单个线路时可以指定线路名(jar同名),不指定随机生成 30 | - jar_suffix 指定spider字段jar包保存后缀名,默认`jar`,一些CDN禁止'jar'后缀,可以修改为`txt`、`json`、`js`、`css`、`html` 31 | - mirror 指定镜像cdn加速,默认mirror=1 32 | - gh1类型 https://raw.githubusercontent.com/fish2018/tvbox/master/all.json => https://xxxx/gh/fish2018/tvbox/all.json 33 | - mirror=1 https://ghp.ci/https://raw.githubusercontent.com 34 | - mirror=2 https://gitdl.cn/https://raw.githubusercontent.com 35 | - mirror=3 https://ghproxy.net/https://raw.githubusercontent.com 36 | - mirror=4 https://github.moeyy.xyz/https://raw.githubusercontent.com 37 | - mirror=5 https://gh-proxy.com/https://raw.githubusercontent.com 38 | - mirror=6 https://ghproxy.cc/https://raw.githubusercontent.com 39 | - mirror=7 https://raw.yzuu.cf 可加速clone、push速度非常快(限制低于50M) 40 | - mirror=8 https://raw.nuaa.cf 41 | - mirror=9 https://raw.kkgithub.com 42 | - mirror=10 https://gh.con.sh/https://raw.githubusercontent.com 43 | - mirror=11 https://gh.llkk.cc/https://raw.githubusercontent.com 44 | - mirror=12 https://gh.ddlc.top/https://raw.githubusercontent.com 45 | - mirror=13 https://gh-proxy.llyke.com/https://raw.githubusercontent.com 46 | - gh2类型(缓存不能及时更新,禁止缓存jar后缀,建议txt、json、js、css、html) https://raw.githubusercontent.com/fish2018/tvbox/master/all.json => https://xxxx/fish2018/tvbox/master/all.json 47 | - mirror=21 https://fastly.jsdelivr.net 48 | - mirror=22 https://jsd.onmicrosoft.cn 49 | - mirror=23 https://gcore.jsdelivr.net 50 | - mirror=24 https://cdn.jsdmirror.com 51 | - mirror=25 https://cdn.jsdmirror.cn 52 | - mirror=26 https://jsd.proxy.aks.moe 53 | - mirror=27 https://jsdelivr.b-cdn.net 54 | - mirror=28 https://jsdelivr.pai233.top 55 | 56 | #### Docker执行示例: 57 | Docker镜像`2011820123/tvbox`,也可以使用代理拉取镜像`dockerproxy.com/2011820123/tvbox:latest`
58 | 首先在github.com上创建自己的代码仓库,推荐命名'tvbox',其他仓库名需要指定参数repo
59 | 支持多url下载,英文逗号`,`分隔多个url,`?&signame={name}`指定单线路名,不指定会生成随机名,{target}.json以最后一个url为准。
60 | 61 | ```bash 62 | docker run --rm -e username=xxx -e token=xxx -e url='http://肥猫.com?&signame=肥猫,http://www.饭太硬.com/tv/?&signame=饭太硬' 2011820123/tvbox 63 | ``` 64 | 65 | 演示: 66 | 67 | ``` 68 | docker run --rm -e repo=ol -e mirror=2 -e jar_suffix=css -e token=XXX -e username=fish2018 -e num=1 -e url='https://www.iyouhun.com/tv/0' 2011820123/tvbox 69 | 70 | >>> 71 | 72 | 开始克隆:git clone https://githubfast.com/fish2018/ol.git 73 | --------- 开始私有化在线接口 ---------- 74 | 当前url: https://www.iyouhun.com/tv/0 75 | 【多仓】 🌹游魂主仓库🌹.json: https://xn--s6wu47g.u.nxog.top/nxog/ou1.php?b=游魂 76 | 开始下载【线路】游魂家庭1: https://xn--s6wu47g.u.nxog.top/m/111.php?ou=公众号欧歌app&mz=index&jar=index&123&b=游魂 77 | 开始下载【线路】游魂云盘2: https://xn--s6wu47g.u.nxog.top/m/111.php?ou=公众号欧歌app&mz=all&jar=all&b=游魂 78 | 开始下载【线路】游魂学习3: https://xn--s6wu47g.u.nxog.top/m/111.php?ou=公众号欧歌app&mz=a3&jar=a3&b=游魂 79 | 开始下载【线路】下面游魂收集网络: https://xn--s6wu47g.u.nxog.top/m/111.php?ou=公众号欧歌app&mz=index&jar=index&321&b=游魂 80 | 开始下载【线路】饭太硬: http://py.nxog.top/?ou=http://www.饭太硬.com/tv/ 81 | 开始下载【线路】OK: http://py.nxog.top/?ou=http://ok321.top/ok 82 | 开始下载【线路】盒子迷: http://py.nxog.top/?ou=https://盒子迷.top/禁止贩卖 83 | 开始下载【线路】D佬: https://download.kstore.space/download/2883/nzk/nzk0722.json 84 | 开始下载【线路】PG: https://gh.con.sh/https://raw.githubusercontent.com/ouhaibo1980/tvbox/master/pg/jsm.json 85 | 开始下载【线路】肥猫: http://py.nxog.top/?ou=http://肥猫.com 86 | 开始下载【线路】小米: http://py.nxog.top/?ou=http://www.mpanso.com/%E5%B0%8F%E7%B1%B3/DEMO.json 87 | 开始下载【线路】放牛: http://py.nxog.top/?ou=http://tvbox.xn--4kq62z5rby2qupq9ub.top 88 | 开始下载【线路】小马: https://szyyds.cn/tv/x.json 89 | 开始下载【线路】天天开心: http://ttkx.live:55/天天开心 90 | 开始下载【线路】摸鱼: http://我不是.摸鱼儿.top 91 | 开始下载【线路】老刘备: https://raw.liucn.cc/box/m.json 92 | 开始下载【线路】香雅情: https://gh.con.sh/https://raw.githubusercontent.com/xyq254245/xyqonlinerule/main/XYQTVBox.json 93 | 开始下载【线路】俊佬: http://home.jundie.top:81/top98.json 94 | 开始下载【线路】月光: https://gh.con.sh/https://raw.githubusercontent.com/guot55/yg/main/max.json 95 | 开始下载【线路】巧技: http://cdn.qiaoji8.com/tvbox.json 96 | 开始下载【线路】荷城茶秀: https://gh.con.sh/https://raw.githubusercontent.com/HeChengChaXiu/tvbox/main/hccx.json 97 | 开始下载【线路】云星日记: http://itvbox.cc/云星日记 98 | 开始下载【线路】吾爱: http://52pan.top:81/api/v3/file/get/174964/%E5%90%BE%E7%88%B1%E8%AF%84%E6%B5%8B.m3u?sign=rPssLoffquDXszCARt6UNF8MobSa1FA27XomzOluJBY%3D%3A0 99 | 开始下载【线路】南风: https://gh.con.sh/https://raw.githubusercontent.com/yoursmile66/TVBox/main/XC.json 100 | 开始下载【线路】2游魂收集不分排名: https://xn--s6wu47g.u.nxog.top/m/333.php?ou=公众号欧歌app&mz=all&jar=all&b=游魂 101 | 开始写入单仓🌹游魂主仓库🌹.json 102 | 开始写入tvbox.json 103 | 开始写入all.json 104 | --------- 完成私有化在线接口 ---------- 105 | 开始推送:git push https://githubfast.com/fish2018/ol.git 106 | 耗时: 176.29488706588745 秒 107 | 108 | #################影视仓APP配置接口######################## 109 | 110 | https://gitdl.cn/https://raw.githubusercontent.com/fish2018/ol/main/all.json 111 | https://gitdl.cn/https://raw.githubusercontent.com/fish2018/ol/main/tvbox.json 112 | 113 | ``` 114 | 115 | 116 | ## 更新说明 117 | - V2.5版本 新增三个gh1代理;设置jar_suffix后会自动把历史的jar后缀批量成新的后缀;兼容证书失效的接口 118 | - V2.4版本 mirror=1 https://mirror.ghproxy.com变更为https://ghp.ci;增加mirror=10 119 | - V2.3版本 更新大量cdn支持;默认使用githubfast.com加速clone和push,失败切换hub.yzuu.cf 120 | - V2.2版本 支持通过jar_suffix参数修改jar包后缀 121 | - V2.1版本 支持多种镜像加速,通过mirror={num}指定;当mirror<4时自动设置/etc/hosts加速github.com,解决运行docker的本地网络不能访问github 122 | - V2.0版本 修复指定target生成指定`{target}`.json;支持多url下载,英文逗号分隔多个url,`?&signame={name}`指定单线路名,不指定会生成随机名。例子:url = 'http://肥猫.com?&signame=肥猫,http://www.饭太硬.com/tv/?&signame=饭太硬' 123 | - V1.9版本 移除多线程下载接口;已下载接口不重复下载;支持js动态渲染数据的接口;增加根据文件大小去重线路;单线路下载不指定signame(单线路名)时会生成一个"{随机字符串}.txt";兼容主分支main/master 124 | - V1.8版本 移除agit.ai支持;all.json线路排序; 125 | - V1.7版本 优化git clone速度,仓库重置提交记录计数(始终commit 1,使仓库存储占用小,下载速度快) 126 | - V1.6版本 不规范json兼容优化,http请求timeout默认3s,优化移除emoji表情 127 | - V1.5版本 bug修复,github.com支持优化 128 | - V1.4版本 bug修复,jar下载失败,不会创建0字节jar文件,保留原jar链接 129 | - V1.3版本 支持github.com 130 | - V1.2版本 支持jar本地化 131 | - V1.1版本 bug修复,仅支持agit.ai,不支持jar本地化 132 | - V1.0版本 支持单线路、单仓、多仓下载,输出:{target}(默认tvbox.json),和url填写的源内容一致;all.json是仓库中所有下载的历史线路总和,并且去重了内容相同的线路 133 | 134 | 135 | -------------------------------------------------------------------------------- /cnb_tvbox_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # !/usr/bin/env python3 3 | from requests_html import HTMLSession 4 | import pprint 5 | import random 6 | import string 7 | import time 8 | import hashlib 9 | import json 10 | import git # gitpython 11 | import re 12 | import base64 13 | import requests 14 | import asyncio 15 | import aiohttp 16 | from requests.adapters import HTTPAdapter, Retry 17 | import os 18 | import subprocess 19 | import ssl 20 | import shutil 21 | from pathlib import Path 22 | from urllib.parse import urlparse, parse_qs, urljoin 23 | import commentjson 24 | ssl._create_default_https_context = ssl._create_unverified_context 25 | import urllib3 26 | from urllib3.exceptions import InsecureRequestWarning 27 | urllib3.disable_warnings(InsecureRequestWarning) 28 | 29 | global pipes 30 | pipes = set() 31 | 32 | class GetSrc: 33 | def __init__(self, username=None, token=None, url=None, repo=None, num=10, target=None, timeout=3, signame=None, mirror=None, jar_suffix=None, site_down=True): 34 | self.jar_suffix = jar_suffix if jar_suffix else 'jar' 35 | self.site_down = site_down # 是否下载site里的文件到本地 36 | self.mirror = int(str(mirror).strip()) if mirror else 1 37 | self.mirror_proxy = 'https://ghp.ci/https://raw.githubusercontent.com' 38 | self.num = int(num) 39 | self.sep = os.path.sep 40 | self.username = username 41 | self.token = token 42 | self.timeout=timeout 43 | self.url = url.replace(' ','').replace(',',',') if url else url 44 | self.repo = repo if repo else 'tvbox' 45 | self.target = f'{target.split(".json")[0]}.json' if target else 'tvbox.json' 46 | self.headers = {"user-agent": "okhttp/3.15 Html5Plus/1.0 (Immersed/23.92157)"} 47 | self.s = requests.Session() 48 | self.signame = signame 49 | retries = Retry(total=3, backoff_factor=1) 50 | self.s.mount('http://', HTTPAdapter(max_retries=retries)) 51 | self.s.mount('https://', HTTPAdapter(max_retries=retries)) 52 | self.size_tolerance = 15 # 线路文件大小误差在15以内认为是同一个 53 | self.main_branch = 'main' 54 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 55 | self.cnb_slot = f'https://cnb.cool/{self.username}/{self.repo}/-/git/raw/{self.main_branch}' 56 | 57 | self.registry = 'cnb.cool' 58 | self.gitusername = 'cnb' 59 | self.repo = repo 60 | self.token = token 61 | self.branch = 'main' 62 | 63 | self.gh1 = [ 64 | 'https://ghp.ci/https://raw.githubusercontent.com', 65 | 'https://gh.xxooo.cf/https://raw.githubusercontent.com', 66 | 'https://ghproxy.net/https://raw.githubusercontent.com', 67 | 'https://github.moeyy.xyz/https://raw.githubusercontent.com', 68 | 'https://gh-proxy.com/https://raw.githubusercontent.com', 69 | 'https://ghproxy.cc/https://raw.githubusercontent.com', 70 | 'https://raw.yzuu.cf', 71 | 'https://raw.nuaa.cf', 72 | 'https://raw.kkgithub.com', 73 | 'https://mirror.ghproxy.com/https://raw.githubusercontent.com', 74 | 'https://gh.llkk.cc/https://raw.githubusercontent.com', 75 | 'https://gh.ddlc.top/https://raw.githubusercontent.com', 76 | 'https://gh-proxy.llyke.com/https://raw.githubusercontent.com', 77 | 'https://slink.ltd', 78 | 'https://cors.zme.ink', 79 | 'https://git.886.be' 80 | ] 81 | self.gh2 = [ 82 | "https://fastly.jsdelivr.net/gh", 83 | "https://jsd.onmicrosoft.cn/gh", 84 | "https://gcore.jsdelivr.net/gh", 85 | "https://cdn.jsdmirror.com/gh", 86 | "https://cdn.jsdmirror.cn/gh", 87 | "https://jsd.proxy.aks.moe/gh", 88 | "https://jsdelivr.b-cdn.net/gh", 89 | "https://jsdelivr.pai233.top/gh" 90 | ] 91 | 92 | # 定义 drpy2 文件列表 93 | self.drpy2 = False 94 | self.drpy2_files = [ 95 | "cat.js", "crypto-js.js", "drpy2.min.js", "http.js", "jquery.min.js", 96 | "jsencrypt.js", "log.js", "pako.min.js", "similarity.js", "uri.min.js", 97 | "cheerio.min.js", "deep.parse.js", "gbk.js", "jinja.js", "json5.js", 98 | "node-rsa.js", "script.js", "spider.js", "模板.js", "quark.min.js" 99 | ] 100 | 101 | async def download_drpy2_files(self): 102 | """ 103 | 异步下载 drpy2 文件到 self.repo/api/drpy2 104 | """ 105 | # 创建 drpy2 目录 106 | api_drpy2_dir = os.path.join(self.repo, "api/drpy2") 107 | if not os.path.exists(api_drpy2_dir): 108 | os.makedirs(api_drpy2_dir) 109 | async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False), 110 | timeout=aiohttp.ClientTimeout(total=60, connect=15) 111 | ) as session: 112 | tasks = [] 113 | for filename in self.drpy2_files: 114 | local_path = os.path.join(api_drpy2_dir, filename) 115 | if os.path.exists(local_path): 116 | # print(f"文件已存在,跳过: {local_path}") 117 | continue 118 | json_url = f"https://github.moeyy.xyz/https://raw.githubusercontent.com/fish2018/lib/main/js/dr_py/{filename}" 119 | 120 | async def download_task(json_url=json_url, local_path=local_path, filename=filename): 121 | retries = 3 122 | for attempt in range(retries): 123 | try: 124 | async with session.get(json_url) as response: 125 | response.raise_for_status() 126 | content = await response.read() 127 | with open(local_path, "wb") as f: 128 | f.write(content) 129 | # print(f"下载成功: {filename}") 130 | return True 131 | except Exception as e: 132 | # print(f"下载 {json_url} 失败 (尝试 {attempt + 1}/{retries}): {e}") 133 | if attempt < retries - 1: 134 | await asyncio.sleep(1) 135 | else: 136 | print(f"下载 {json_url} 最终失败") 137 | return False 138 | 139 | tasks.append(download_task()) 140 | 141 | if tasks: 142 | # print(f"开始下载 {len(tasks)} 个 drpy2 文件") 143 | await asyncio.gather(*tasks, return_exceptions=True) 144 | else: 145 | pass 146 | # print("所有 drpy2 文件已存在,无需下载") 147 | def file_hash(self, filepath): 148 | with open(filepath, 'rb') as f: 149 | file_contents = f.read() 150 | return hashlib.sha256(file_contents).hexdigest() 151 | def remove_duplicates(self, folder_path): 152 | folder_path = Path(folder_path) 153 | jar_folder = f'{folder_path}/jar' 154 | excludes = {'.json', '.git', 'jar', '.idea', 'ext', '.DS_Store', '.md'} 155 | files_info = {} 156 | 157 | # 把jar目录下所有文件后缀都改成新的self.jar_suffix 158 | self.rename_jar_suffix(jar_folder) 159 | 160 | # 存储文件名、大小和哈希值 161 | for file_path in folder_path.iterdir(): 162 | if file_path.is_file() and file_path.suffix not in excludes: 163 | file_size = file_path.stat().st_size 164 | file_hash = self.file_hash(file_path) 165 | files_info[file_path.name] = {'path': str(file_path), 'size': file_size, 'hash': file_hash} 166 | 167 | # 保留的文件列表 168 | keep_files = [] 169 | # 按文件大小排序,然后按顺序处理 170 | for file_name, info in sorted(files_info.items(), key=lambda item: item[1]['size']): 171 | if not keep_files or abs(info['size'] - files_info[keep_files[-1]]['size']) > self.size_tolerance: 172 | keep_files.append(file_name) 173 | # 删除jar目录下除了{self.jar_suffix}的文件 174 | # self.remove_all_except_jar(jar_folder) 175 | else: 176 | # 如果当前文件大小在容忍范围内,删除当前文件和对应的jar文件 177 | os.remove(info['path']) 178 | self.remove_jar_file(jar_folder, file_name.replace('.txt', f'{self.jar_suffix}')) 179 | 180 | keep_files.sort() 181 | return keep_files 182 | def rename_jar_suffix(self,jar_folder): 183 | # 遍历目录中的所有文件和子目录 184 | for root, dirs, files in os.walk(jar_folder): 185 | for file in files: 186 | # 构造完整的文件路径 187 | old_file = os.path.join(root, file) 188 | # 构造新的文件名,去除原有的后缀,加上 self.jar_suffix 189 | new_file = os.path.join(root, os.path.splitext(file)[0] + f'.{self.jar_suffix}') 190 | # 重命名文件 191 | os.rename(old_file, new_file) 192 | # print(f"文件已重命名: {old_file} -> {new_file}") 193 | def remove_all_except_jar(self, jar_folder): 194 | # 列出文件夹中的所有文件 195 | for file_name in os.listdir(jar_folder): 196 | # 构建完整的文件路径 197 | full_path = os.path.join(jar_folder, file_name) 198 | # 检查是否为文件 199 | if os.path.isfile(full_path): 200 | # 获取文件的扩展名 201 | _, file_extension = os.path.splitext(file_name) 202 | # 如果扩展名不是self.jar_suffix,则删除文件 203 | if file_extension != f'.{self.jar_suffix}': 204 | self.remove_jar_file(jar_folder, file_name) 205 | def remove_jar_file(self, jar_folder, file_name): 206 | # 构建jar文件的路径 207 | jar_file_path = os.path.join(jar_folder, file_name) 208 | # 如果jar文件存在,则删除它 209 | if os.path.isfile(jar_file_path): 210 | os.remove(jar_file_path) 211 | def remove_emojis(self, text): 212 | emoji_pattern = re.compile("[" 213 | u"\U0001F600-\U0001F64F" # emoticons 214 | u"\U0001F300-\U0001F5FF" # symbols & pictographs 215 | u"\U0001F680-\U0001F6FF" # transport & map symbols 216 | u"\U0001F1E0-\U0001F1FF" # flags (iOS) 217 | "\U00002500-\U00002BEF" # chinese char 218 | "\U00010000-\U0010ffff" 219 | "\u200d" # zero width joiner 220 | "\u20E3" # combining enclosing keycap 221 | "\ufe0f" # VARIATION SELECTOR-16 222 | "]+", flags=re.UNICODE) 223 | text = text.replace('/', '_').replace('多多', '').replace('┃', '').replace('线路', '').replace('匚','').strip() 224 | return emoji_pattern.sub('', text) 225 | def json_compatible(self, str): 226 | # 兼容错误json 227 | # res = str.replace(' ', '').replace("'",'"').replace('//"', '"').replace('//{', '{').replace('key:', '"key":').replace('name:', '"name":').replace('type:', '"type":').replace('api:','"api":').replace('searchable:', '"searchable":').replace('quickSearch:', '"quickSearch":').replace('filterable:','"filterable":').strip() 228 | res = str.replace('//"', '"').replace('//{', '{').replace('key:', '"key":').replace('name:', '"name":').replace('type:', '"type":').replace('api:','"api":').replace('searchable:', '"searchable":').replace('quickSearch:', '"quickSearch":').replace('filterable:','"filterable":').strip() 229 | return res 230 | def ghproxy(self, str): 231 | u = 'https://github.moeyy.xyz/' 232 | res = str.replace('https://ghproxy.net/', u).replace('https://ghproxy.com/', u).replace('https://gh-proxy.com/',u).replace('https://mirror.ghproxy.com/',u).replace('https://gh.xxooo.cf/',u).replace('https://ghp.ci/',u).replace('https://gitdl.cn/',u) 233 | return res 234 | def set_hosts(self): 235 | # 设置github.com的加速hosts 236 | try: 237 | response = requests.get('https://hosts.gitcdn.top/hosts.json') 238 | if response.status_code == 200: 239 | hosts_data = response.json() 240 | # 遍历JSON数据,找到"github.com"对应的IP 241 | github_ip = None 242 | for entry in hosts_data: 243 | if entry[1] == "github.com": 244 | github_ip = entry[0] 245 | break 246 | if github_ip: 247 | # 读取现有的/etc/hosts文件 248 | with open('/etc/hosts', 'r+') as file: 249 | hosts_content = file.read() 250 | # 检查是否已经存在对应的IP 251 | if github_ip not in hosts_content: 252 | # 将新的IP添加到文件末尾 253 | file.write(f'\n{github_ip} github.com') 254 | print(f'IP address {github_ip} for github.com has been added to /etc/hosts.') 255 | else: 256 | print(f'IP address for github.com is already in /etc/hosts.') 257 | else: 258 | print('No IP found for github.com in the provided data.') 259 | else: 260 | print('Failed to retrieve data from https://hosts.gitcdn.top/hosts.json') 261 | except Exception as e: 262 | pass 263 | def picparse(self, url): 264 | r = self.s.get(url, headers=self.headers, timeout=self.timeout, verify=False) 265 | pattern = r'([A-Za-z0-9+/]+={0,2})' 266 | matches = re.findall(pattern, r.text) 267 | decoded_data = base64.b64decode(matches[-1]) 268 | text = decoded_data.decode('utf-8') 269 | return text 270 | 271 | async def js_render(self, url): 272 | # 获取 JS 渲染页面源代码 273 | timeout = self.timeout * 4 274 | if timeout > 15: 275 | timeout = 15 276 | browser_args = [ 277 | '--no-sandbox', 278 | '--disable-dev-shm-usage', 279 | '--disable-gpu', 280 | '--disable-software-rasterizer', 281 | '--disable-setuid-sandbox', 282 | ] 283 | from requests_html import AsyncHTMLSession 284 | 285 | session = AsyncHTMLSession(browser_args=browser_args) 286 | try: 287 | r = await session.get( 288 | f'http://lige.unaux.com/?url={url}', 289 | headers=self.headers, 290 | timeout=timeout, 291 | verify=False, 292 | ) 293 | # 等待页面加载完成并渲染 JavaScript 294 | await r.html.arender(timeout=timeout) 295 | return r.html 296 | finally: 297 | await session.close() 298 | async def site_file_down(self, files, url): 299 | """ 300 | 异步函数,用于同时下载和更新 ext、jar 和 api 文件。 301 | 302 | 参数: 303 | files: 包含一个或两个文件路径的列表,例如 ['api.json', 'output.json'] 304 | url: 基础 URL,用于构造完整的下载 URL 305 | """ 306 | # 设置 ext、jar 和 api 的保存目录 307 | ext_dir = f"{self.repo}/ext" 308 | jar_dir = f"{self.repo}/jar" 309 | api_dir = f"{self.repo}/api" 310 | api_drpy2_dir = f"{self.repo}/api/drpy2" 311 | for directory in [ext_dir, jar_dir, api_dir, api_drpy2_dir]: 312 | if not os.path.exists(directory): 313 | os.makedirs(directory) 314 | 315 | # 获取文件路径并读取 api.json 316 | file = files[0] 317 | file2 = files[1] if len(files) > 1 else '' 318 | 319 | with open(file, 'r', encoding='utf-8') as f: 320 | try: 321 | api_data = commentjson.load(f) 322 | sites = api_data["sites"] 323 | print(f"总站点数: {len(sites)}") 324 | except Exception as e: 325 | # print(f"解析 {file} 失败: {e}") 326 | return 327 | 328 | # 使用 aiohttp 创建会话并收集下载任务 329 | async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False), 330 | timeout=aiohttp.ClientTimeout(total=60, connect=15) 331 | ) as session: 332 | tasks = [] 333 | for site in sites: 334 | for field in ["ext", "jar", "api"]: 335 | repo_dir_name = field 336 | if field in site: 337 | value = site[field] 338 | if isinstance(value, str): 339 | clean_value = value.split(';')[0].rstrip('?') 340 | if field == "ext": 341 | if not clean_value.endswith((".js", ".txt", ".json")): 342 | continue 343 | elif field == "api": 344 | if os.path.basename(clean_value).lower() in ["drpy2.min.js","quark.min.js"]: 345 | self.drpy2 = True 346 | # 替换 api 字段 347 | site[field] = f"{self.cnb_slot}/{api_drpy2_dir}/{os.path.basename(clean_value).lower()}" 348 | continue 349 | if not clean_value.endswith(".py"): 350 | continue 351 | 352 | # 默认下载逻辑(ext、jar 和 api 的 .py 文件) 353 | filename = os.path.basename(clean_value) 354 | if './' in value: 355 | path = os.path.dirname(url) 356 | json_url = value.replace('./', f'{path}/') 357 | else: 358 | json_url = urljoin(url, value) 359 | local_path = os.path.join(f"{self.repo}/{repo_dir_name}", filename) 360 | 361 | async def download_task(site=site, json_url=json_url, local_path=local_path, 362 | filename=filename, field=field, repo_dir_name=repo_dir_name): 363 | retries = 3 364 | for attempt in range(retries): 365 | try: 366 | async with session.get(json_url) as response: 367 | response.raise_for_status() 368 | if os.path.exists(local_path): 369 | site[field] = f'{self.cnb_slot}/{repo_dir_name}/{filename}' 370 | return True 371 | content = await response.read() 372 | with open(local_path, "wb") as f: 373 | f.write(content) 374 | site[field] = f'{self.cnb_slot}/{repo_dir_name}/{filename}' 375 | return True 376 | except Exception as e: 377 | # print(f"下载 {json_url} 失败 (尝试 {attempt + 1}/{retries}): {e}") 378 | if attempt < retries - 1: 379 | await asyncio.sleep(1) 380 | else: 381 | # print(f"下载 {json_url} 最终失败") 382 | return False 383 | 384 | tasks.append(download_task()) 385 | 386 | if tasks: 387 | # print(f"总下载任务数: {len(tasks)}") 388 | await asyncio.gather(*tasks, return_exceptions=True) 389 | else: 390 | pass 391 | # print("没有找到符合条件的 ext、jar 或 api 文件需要下载") 392 | 393 | # 将更新后的数据写回文件 394 | with open(file, 'w', encoding='utf-8') as f: 395 | json.dump(api_data, f, indent=4, ensure_ascii=False) 396 | 397 | if file2 and os.path.basename(file2): 398 | with open(file2, 'w', encoding='utf-8') as f: 399 | json.dump(api_data, f, indent=4, ensure_ascii=False) 400 | def get_jar(self, name, url, text): 401 | if not os.path.exists(f'{self.repo}/jar'): 402 | os.makedirs(f'{self.repo}/jar') 403 | name = f'{name}.{self.jar_suffix}' 404 | pattern = r'\"spider\":(\s)?\"([^,]+)\"' 405 | matches = re.search(pattern, text) 406 | try: 407 | jar = matches.group(2).replace('./', f'{url}/').split(';')[0] 408 | jar = jar.split('"spider":"')[-1] 409 | if name==f'{self.repo}.{self.jar_suffix}': 410 | name = f"{jar.split('/')[-1]}" 411 | print('jar地址: ', jar) 412 | timeout = self.timeout * 4 413 | if timeout > 15: 414 | timeout = 15 415 | r = self.s.get(jar, timeout=timeout, verify=False) 416 | if r.status_code != 200: 417 | raise f'【jar下载失败】{name} jar地址: {jar} status_code:{r.status_code}' 418 | with open(f'{self.repo}/jar/{name}', 'wb') as f: 419 | f.write(r.content) 420 | jar = f'{self.cnb_slot}/jar/{name}' 421 | text = text.replace(matches.group(2), jar) 422 | except Exception as e: 423 | print(f'【jar下载失败】{name} jar地址: {jar} error:{e}') 424 | return text 425 | async def download(self, url, name, filename, cang=True): 426 | file_list = [] 427 | for root, dirs, files in os.walk(self.repo): 428 | for file in files: 429 | file_list.append(file) 430 | if filename in file_list: 431 | print(f'{filename}:已经存在,无需重复下载') 432 | return 433 | if 'agit.ai' in url: 434 | print(f'下载异常:agit.ai失效') 435 | return 436 | # 下载单线路 437 | item = {} 438 | try: 439 | path = os.path.dirname(url) 440 | r = self.s.get(url, headers=self.headers, allow_redirects=True, timeout=self.timeout, verify=False) 441 | if r.status_code == 200: 442 | print("开始下载【线路】{}: {}".format(name, url)) 443 | if 'searchable' not in r.text: 444 | r = self.js_render(url) 445 | if not r.text: 446 | r = self.picparse(url) 447 | if 'searchable' not in r: 448 | raise 449 | r = self.get_jar(name, url, r) 450 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f: 451 | f.write(r) 452 | return 453 | if 'searchable' not in r.text: 454 | raise 455 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f: 456 | try: 457 | if r.content.decode('utf8').startswith(u'\ufeff'): 458 | str = r.content.decode('utf8').encode('utf-8')[3:].decode('utf-8') 459 | else: 460 | str = r.content.decode('utf-8').replace('./', f'{path}/') 461 | except: 462 | str = r.text 463 | finally: 464 | r = self.ghproxy(str.replace('./', f'{path}/')) 465 | 466 | r = self.get_jar(name, url, r) 467 | f.write(r) 468 | pipes.add(name) 469 | try: 470 | if self.site_down: 471 | await self.site_file_down([f'{self.repo}{self.sep}{filename}'], url) 472 | except Exception as e: 473 | print(f'下载ext中的json失败: {e}') 474 | except Exception as e: 475 | print(f"【线路】{name}: {url} 下载错误:{e}") 476 | # 单仓时写入item 477 | if os.path.exists(f'{self.repo}{self.sep}{filename}') and cang: 478 | item['name'] = name 479 | item['url'] = f'{self.cnb_slot}/{filename}' 480 | items.append(item) 481 | async def down(self, data, s_name): 482 | ''' 483 | 下载单仓 484 | ''' 485 | newJson = {} 486 | global items 487 | items = [] 488 | urls = data.get("urls") if data.get("urls") else data.get("sites") 489 | for u in urls: 490 | name = u.get("name").strip() 491 | name = self.remove_emojis(name) 492 | url = u.get("url") 493 | url = self.ghproxy(url) 494 | filename = '{}.txt'.format(name) 495 | if name in pipes: 496 | print(f"【线路】{name} 已存在,无需重复下载") 497 | continue 498 | await self.download(url, name, filename) 499 | newJson['urls'] = items 500 | newJson = pprint.pformat(newJson, width=200) 501 | print(f'开始写入单仓{s_name}') 502 | with open(f'{self.repo}{self.sep}{s_name}', 'w+', encoding='utf-8') as f: 503 | content = str(newJson).replace("'", '"') 504 | f.write(json.loads(json.dumps(content, indent=4, ensure_ascii=False))) 505 | def all(self): 506 | # 整合所有文件到all.json 507 | newJson = {} 508 | items = [] 509 | files = self.remove_duplicates(self.repo) 510 | for file in files: 511 | item = {} 512 | item['name'] = file.split('.txt')[0] 513 | item['url'] = f'{self.cnb_slot}/{file}' 514 | items.append(item) 515 | newJson['urls'] = items 516 | newJson = pprint.pformat(newJson, width=200) 517 | print(f'开始写入all.json') 518 | with open(f'{self.repo}{self.sep}all.json', 'w+', encoding='utf-8') as f: 519 | content = str(newJson).replace("'", '"') 520 | f.write(json.loads(json.dumps(content, indent=4, ensure_ascii=False))) 521 | async def batch_handle_online_interface(self): 522 | # 下载线路,处理多url场景 523 | print(f'--------- 开始私有化在线接口 ----------') 524 | urls = self.url.split(',') 525 | for url in urls: 526 | # 解析URL 527 | parsed_url = urlparse(url) 528 | # 获取查询参数 529 | query_params = parse_qs(parsed_url.query) 530 | # 提取'signame'参数的值 531 | signame_value = query_params.get('signame', [''])[0] 532 | item = url.split('?&signame=') 533 | self.url = item[0] 534 | self.signame = signame_value if signame_value else None 535 | print(f'当前url: {self.url}') 536 | await self.storeHouse() 537 | await self.clean_directories() 538 | 539 | async def clean_directories(self): 540 | # Step 1: 删除 api/drpy2 目录(如果 self.drpy2 为假) 541 | if not self.drpy2: 542 | drpy2_path = f"{self.repo}/api/drpy2" 543 | if os.path.exists(drpy2_path): 544 | await asyncio.to_thread(shutil.rmtree, drpy2_path) 545 | 546 | # Step 2: 检查并删除空的 api 和 ext 目录 547 | directories = [f"{self.repo}/api", f"{self.repo}/ext"] 548 | for dir_path in directories: 549 | if os.path.exists(dir_path) and os.path.isdir(dir_path): 550 | if not os.listdir(dir_path): # 目录为空 551 | await asyncio.to_thread(shutil.rmtree, dir_path) 552 | def git_clone(self): 553 | self.domain = f'https://{self.gitusername}:{self.token}@{self.registry}/{self.username}/{self.repo}.git' 554 | if os.path.exists(self.repo): 555 | subprocess.call(['rm', '-rf', self.repo]) 556 | try: 557 | print(f'开始克隆:git clone https://{self.registry}/{self.username}/{self.repo}.git') 558 | git.Repo.clone_from(self.domain, to_path=self.repo, depth=1) 559 | except Exception as e: 560 | print(222222, e) 561 | def get_local_repo(self): 562 | # 打开本地仓库,读取仓库信息 563 | repo = git.Repo(self.repo) 564 | config_writer = repo.config_writer() 565 | config_writer.set_value('user', 'name', self.username) 566 | config_writer.set_value('user', 'email', self.username) 567 | # 设置 http.postBuffer 568 | config_writer.set_value('http', 'postBuffer', '73400320') 569 | config_writer.release() 570 | # 获取远程仓库的引用 571 | remote = repo.remote(name='origin') 572 | # 获取远程分支列表 573 | remote_branches = remote.refs 574 | # 遍历远程分支,查找主分支 575 | for branch in remote_branches: 576 | if branch.name == 'origin/master' or branch.name == 'origin/main': 577 | self.branch = branch.name.split('/')[-1] 578 | break 579 | # print(f"仓库{self.repo} 主分支为: {self.main_branch}") 580 | return repo 581 | def reset_commit(self,repo): 582 | # 重置commit 583 | try: 584 | os.chdir(self.repo) 585 | # print('开始清理git',os.getcwd()) 586 | repo.git.checkout('--orphan', 'tmp_branch') 587 | repo.git.add(A=True) 588 | repo.git.commit(m="update") 589 | repo.git.execute(['git', 'branch', '-D', self.branch]) 590 | repo.git.execute(['git', 'branch', '-m', self.branch]) 591 | repo.git.execute(['git', 'push', '-f', 'origin', self.branch]) 592 | except Exception as e: 593 | print('git清理异常', e) 594 | def git_push(self, repo): 595 | # 推送并重置commit计数 596 | # 推送 597 | print(f'开始推送:git push https://{self.registry}/{self.username}/{self.repo}.git') 598 | try: 599 | repo.git.add(A=True) 600 | repo.git.commit(m="update") 601 | repo.git.push() 602 | self.reset_commit(repo) 603 | except Exception as e: 604 | try: 605 | repo.git.execute(['git', 'push', '--set-upstream', 'origin', self.branch]) 606 | self.reset_commit(repo) 607 | except Exception as e: 608 | print('git推送异常', e) 609 | async def storeHouse(self): 610 | ''' 611 | 生成多仓json文件 612 | ''' 613 | await self.download_drpy2_files() 614 | 615 | newJson = {} 616 | items = [] 617 | 618 | # 解析最初链接 619 | try: 620 | res = self.s.get(self.url, headers=self.headers, verify=False, timeout=self.timeout).content.decode('utf8') 621 | if '404 Not Found' in res: 622 | print(f'{self.url} 请求异常') 623 | return 624 | except Exception as e: 625 | if 'Read timed out' in str(e) or 'nodename nor servname provided, or not known' in str(e): 626 | print(f'{self.url} 请求异常:{e}') 627 | return 628 | html = await self.js_render(self.url) 629 | res = html.text.replace(' ', '').replace("'", '"') 630 | if 'Read timed out' in str(e) or 'nodename nor servname provided, or not known' in str(e): 631 | print(f'{self.url} 请求异常:{e}') 632 | return 633 | if not res: 634 | res = self.picparse(self.url).replace(' ', '').replace("'", '"') 635 | 636 | # 线路 637 | if 'searchable' in str(res): 638 | filename = self.signame + '.txt' if self.signame else f"{''.join(random.choices(string.ascii_letters + string.digits, k=10))}.txt" 639 | path = os.path.dirname(self.url) 640 | print("【线路】 {}: {}".format(self.repo, self.url)) 641 | try: 642 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f, open( 643 | f'{self.repo}{self.sep}{self.target}', 'w+', encoding='utf-8') as f2: 644 | r = self.ghproxy(res.replace('./', f'{path}/')) 645 | r = self.get_jar(filename.split('.txt')[0], url, r) 646 | # json容错处理 647 | r = self.json_compatible(r) 648 | f.write(r) 649 | f2.write(r) 650 | except Exception as e: 651 | print(333333333, e) 652 | try: 653 | if self.site_down: 654 | await self.site_file_down([f'{self.repo}{self.sep}{filename}',f'{self.repo}{self.sep}{self.target}'], self.url) 655 | except Exception as e: 656 | pass 657 | return 658 | 659 | # json容错处理 660 | res = self.json_compatible(res) 661 | # 移除注释 662 | datas = '' 663 | for d in res.splitlines(): 664 | if d.find(" //") != -1 or d.find("// ") != -1 or d.find(",//") != -1 or d.startswith("//"): 665 | d = d.split(" //", maxsplit=1)[0] 666 | d = d.split("// ", maxsplit=1)[0] 667 | d = d.split(",//", maxsplit=1)[0] 668 | d = d.split("//", maxsplit=1)[0] 669 | datas = '\n'.join([datas, d]) 670 | # 容错处理,便于json解析 671 | datas = datas.replace('\n', '') 672 | res = datas.replace(' ', '').replace("'", '"').replace('\n', '') 673 | if datas.startswith(u'\ufeff'): 674 | try: 675 | res = datas.encode('utf-8')[3:].decode('utf-8').replace(' ', '').replace("'", '"').replace('\n', '') 676 | except Exception as e: 677 | res = datas.encode('utf-8')[4:].decode('utf-8').replace(' ', '').replace("'", '"').replace('\n', '') 678 | 679 | # 多仓 680 | elif 'storeHouse' in datas: 681 | res = json.loads(str(res)) 682 | srcs = res.get("storeHouse") if res.get("storeHouse") else None 683 | if srcs: 684 | i = 1 685 | for s in srcs: 686 | if i > self.num: 687 | break 688 | i += 1 689 | item = {} 690 | s_name = s.get("sourceName") 691 | s_name = self.remove_emojis(s_name) 692 | s_name = f'{s_name}.json' 693 | s_url = s.get("sourceUrl") 694 | print("【多仓】 {}: {}".format(s_name, s_url)) 695 | try: 696 | if self.s.get(s_url, headers=self.headers).status_code >= 400: 697 | continue 698 | except Exception as e: 699 | print('地址无法响应: ',e) 700 | continue 701 | try: 702 | if self.s.get(s_url, headers=self.headers).content.decode('utf8').lstrip().startswith(u'\ufeff'): 703 | data = self.s.get(s_url, headers=self.headers).content.decode('utf-8')[1:] 704 | else: 705 | data = self.s.get(s_url, headers=self.headers).content.decode('utf-8') 706 | except Exception as e: 707 | try: 708 | data = self.s.get(s_url, headers=self.headers).content.decode('utf8') 709 | data = data.encode('utf-8').decode('utf-8') 710 | except Exception as e: 711 | continue 712 | datas = '' 713 | for d in data.splitlines(): 714 | if d.find(" //") != -1 or d.find("// ") != -1 or d.find(",//") != -1 or d.startswith("//"): 715 | d = d.split(" //", maxsplit=1)[0] 716 | d = d.split("// ", maxsplit=1)[0] 717 | d = d.split(",//", maxsplit=1)[0] 718 | d = d.split("//", maxsplit=1)[0] 719 | datas = '\n'.join([datas, d]) 720 | 721 | try: 722 | if datas.lstrip().startswith(u'\ufeff'): 723 | datas = datas.encode('utf-8')[1:] 724 | await self.down(json.loads(datas), s_name) 725 | except Exception as e: 726 | try: 727 | data = self.s.get(s_url, headers=self.headers).text 728 | except Exception as e: 729 | continue 730 | datas = '' 731 | for d in data.splitlines(): 732 | datas += d.replace('\n', '').replace(' ', '').strip() 733 | datas = datas.encode('utf-8') 734 | if 'DOCTYPEhtml' in str(datas): 735 | continue 736 | datas = re.sub(r'^(.*?)\{', '{', datas.decode('utf-8'), flags=re.DOTALL | re.MULTILINE) 737 | await self.down(json.loads(datas), s_name) 738 | item['sourceName'] = s_name.split('.json')[0] 739 | item['sourceUrl'] = f'{self.cnb_slot}/{s_name}' 740 | items.append(item) 741 | newJson["storeHouse"] = items 742 | newJson = pprint.pformat(newJson, width=200) 743 | with open(f'{self.repo}{self.sep}{self.target}', 'w+', encoding='utf-8') as f: 744 | print(f"开始写入{self.target}") 745 | f.write(json.dumps(json.loads(str(newJson).replace("'", '"')), sort_keys=True, indent=4, ensure_ascii=False)) 746 | # 单仓 747 | else: 748 | try: 749 | res = json.loads(str(res)) 750 | except Exception as e: 751 | if 'domainnameisinvalid' in res: 752 | print(f'该域名无效,请提供正常可用接口') 753 | return 754 | html = await self.js_render(self.url) 755 | res = html.text.replace(' ', '').replace("'", '"') 756 | if not res: 757 | res = self.picparse(self.url).replace(' ', '').replace("'", '"') 758 | try: 759 | res = json.loads(str(res)) 760 | except Exception as e: 761 | # print(111111, e, res) 762 | pass 763 | s_name = self.target 764 | s_url = self.url 765 | print("【单仓】 {}: {}".format(s_name, s_url)) 766 | try: 767 | await self.down(res, s_name) 768 | except Exception as e: 769 | if 'searchable' in str(res): 770 | filename = self.signame + '.txt' if self.signame else f"{''.join(random.choices(string.ascii_letters + string.digits, k=10))}.txt" 771 | print("【线路】 {}: {}".format(filename, self.url)) 772 | try: 773 | await self.download(self.url, filename.split('.txt')[0], filename, cang=False) 774 | except Exception as e: 775 | print('下载异常', e) 776 | def replace_urls_gh1(self, content): 777 | # 适用于https://ghp.ci/https://raw.githubusercontent.com|https://raw.yzuu.cf类型 778 | # 使用正则表达式查找并替换链接 779 | def replace_match(match): 780 | username = match.group(2) 781 | repo_name = match.group(3) 782 | path = match.group(4) 783 | # 构建新的URL 784 | new_url = f"{self.mirror_proxy}/{username}/{repo_name}/{self.main_branch}" 785 | if path: 786 | new_url += path 787 | return new_url 788 | return self.pattern.sub(replace_match, content) 789 | def replace_urls_gh2(self, content): 790 | # 适用于https://gcore/jsdelivr.net/gh类型 791 | # 替换函数 792 | def replace_match(match): 793 | return f'{self.mirror_proxy}/{match.group(2)}/{match.group(3)}{match.group(5)}' 794 | # 执行替换 795 | return self.pattern.sub(replace_match, content) 796 | def mirror_proxy2new(self): 797 | # 把文本文件中所有镜像代理路径替换掉 798 | if self.mirror < 20: 799 | # gh1 适用于https://ghp.ci/https://raw.githubusercontent.com|https://raw.yzuu.cf类型 800 | # 自动转换提供的域名列表为正则表达式所需的格式 801 | patterns = [re.escape(proxy) for proxy in self.gh2] 802 | # 组合成一个正则表达式 803 | self.pattern = re.compile(r'({})/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_-]+)(/.*)?'.format('|'.join(patterns))) 804 | # 遍历文件夹中的所有文件 805 | for root, dirs, files in os.walk(self.repo): 806 | for file in files: 807 | if file.endswith('.txt') or file.endswith('.json'): 808 | file_path = os.path.join(root, file) 809 | with open(file_path, 'r', encoding='utf-8') as f: 810 | content = f.read() 811 | # 替换文件中的URL 812 | new_content = self.replace_urls_gh1(content) 813 | for i in self.gh1: 814 | new_content = new_content.replace(i,self.mirror_proxy) 815 | with open(file_path, 'w', encoding='utf-8') as f: 816 | f.write(new_content) 817 | elif self.mirror > 20: 818 | # gh2适用于https://gcore.jsdelivr.net/gh类型 819 | if self.jar_suffix not in ['html','js','css','json','txt']: 820 | return 821 | # 自动转换提供的域名列表为正则表达式所需的格式 822 | patterns = [re.escape(proxy) for proxy in self.gh1] 823 | # 组合成一个正则表达式 824 | self.pattern = re.compile(r'({})/(.+?)/(.+?)/(master|main)(/|/.*)'.format('|'.join(patterns))) 825 | # 遍历文件夹中的所有文件 826 | for filename in os.listdir(self.repo): 827 | file_path = os.path.join(self.repo, filename) 828 | # 检查是否是文件而不是文件夹 829 | if os.path.isfile(file_path) and (filename.endswith('.txt') or filename.endswith('.json')): 830 | # 读取文件内容 831 | with open(file_path, 'r', encoding='utf-8') as file: 832 | content = file.read() 833 | # 替换文件中的URL 834 | new_content = self.replace_urls_gh2(content) 835 | for i in self.gh2: 836 | new_content = new_content.replace(i,self.mirror_proxy) 837 | # 写回文件 838 | with open(file_path, 'w', encoding='utf-8') as f: 839 | f.write(new_content) 840 | def mirror_init(self): 841 | # gh1 842 | if self.mirror == 1: 843 | self.mirror_proxy = 'https://ghp.ci/https://raw.githubusercontent.com' 844 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 845 | elif self.mirror == 2: 846 | self.mirror_proxy = 'https://gh.xxooo.cf/https://raw.githubusercontent.com' 847 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 848 | elif self.mirror == 3: 849 | self.mirror_proxy = 'https://ghproxy.net/https://raw.githubusercontent.com' 850 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 851 | elif self.mirror == 4: 852 | self.mirror_proxy = 'https://github.moeyy.xyz/https://raw.githubusercontent.com' 853 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 854 | elif self.mirror == 5: 855 | self.mirror_proxy = 'https://gh-proxy.com/https://raw.githubusercontent.com' 856 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 857 | elif self.mirror == 6: 858 | self.mirror_proxy = 'https://ghproxy.cc/https://raw.githubusercontent.com' 859 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 860 | elif self.mirror == 7: 861 | self.mirror_proxy = 'https://raw.yzuu.cf' 862 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 863 | # self.registry = 'hub.yzuu.cf' 864 | elif self.mirror == 8: 865 | self.mirror_proxy = 'https://raw.nuaa.cf' 866 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 867 | elif self.mirror == 9: 868 | self.mirror_proxy = 'https://raw.kkgithub.com' 869 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 870 | elif self.mirror == 10: 871 | self.mirror_proxy = 'https://gh.con.sh/https://raw.githubusercontent.com' 872 | elif self.mirror == 11: 873 | self.mirror_proxy = 'https://gh.llkk.cc/https://raw.githubusercontent.com' 874 | elif self.mirror == 12: 875 | self.mirror_proxy = 'https://gh.ddlc.top/https://raw.githubusercontent.com' 876 | elif self.mirror == 13: 877 | self.mirror_proxy = 'https://gh-proxy.llyke.com/https://raw.githubusercontent.com' 878 | 879 | # gh2 880 | elif self.mirror == 21: 881 | self.mirror_proxy = "https://fastly.jsdelivr.net/gh" 882 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 883 | elif self.mirror == 22: 884 | self.mirror_proxy = "https://jsd.onmicrosoft.cn/gh" 885 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 886 | elif self.mirror == 23: 887 | self.mirror_proxy = "https://gcore.jsdelivr.net/gh" 888 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 889 | elif self.mirror == 24: 890 | self.mirror_proxy = "https://cdn.jsdmirror.com/gh" 891 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 892 | elif self.mirror == 25: 893 | self.mirror_proxy = "https://cdn.jsdmirror.cn/gh" 894 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 895 | elif self.mirror == 26: 896 | self.mirror_proxy = "https://jsd.proxy.aks.moe/gh" 897 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 898 | elif self.mirror == 27: 899 | self.mirror_proxy = "https://jsdelivr.b-cdn.net/gh" 900 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 901 | elif self.mirror == 28: 902 | self.mirror_proxy = "https://jsdelivr.pai233.top/gh" 903 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 904 | 905 | def run(self): 906 | start_time = time.time() 907 | self.git_clone() 908 | asyncio.run(self.batch_handle_online_interface()) 909 | repo = self.get_local_repo() 910 | self.all() 911 | self.mirror_proxy2new() 912 | self.git_push(repo) 913 | end_time = time.time() 914 | print(f'耗时: {end_time - start_time} 秒\n\n#################影视仓APP配置接口########################\n\n{self.cnb_slot}/all.json\n{self.cnb_slot}/{self.target}') 915 | 916 | 917 | if __name__ == '__main__': 918 | token = 'xxx' 919 | username = 'fish2018' 920 | repo = 'test' 921 | # url = 'https://github.moeyy.xyz/https://raw.githubusercontent.com/wwb521/live/main/video.json?signame=18' 922 | # url = 'https://github.moeyy.xyz/https://raw.githubusercontent.com/supermeguo/BoxRes/main/Myuse/catcr.json?signame=v18' 923 | # url = 'http://box.ufuzi.com/tv/qq/%E7%9F%AD%E5%89%A7%E9%A2%91%E9%81%93/api.json?signame=duanju' 924 | # url = 'https://肥猫.com?signame=肥猫' 925 | url = 'https://tvbox.catvod.com/xs/api.json?signame=xs' 926 | site_down = True # 将site中的文件下载本地化 927 | GetSrc(username=username, token=token, url=url, repo=repo, mirror=4, num=10, site_down=site_down).run() 928 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | GitPython==3.1.43 2 | requests==2.27.1 3 | requests-html==0.10.0 4 | lxml_html_clean==0.2.1 5 | pyppeteer==2.0.0 6 | -------------------------------------------------------------------------------- /tvbox_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # !/usr/bin/env python3 3 | from requests_html import HTMLSession 4 | import pprint 5 | import random 6 | import string 7 | import time 8 | import hashlib 9 | import json 10 | import git # gitpython 11 | import re 12 | import base64 13 | import requests 14 | from requests.adapters import HTTPAdapter, Retry 15 | import os 16 | import subprocess 17 | import ssl 18 | from pathlib import Path 19 | ssl._create_default_https_context = ssl._create_unverified_context 20 | import urllib3 21 | from urllib3.exceptions import InsecureRequestWarning 22 | urllib3.disable_warnings(InsecureRequestWarning) 23 | 24 | global pipes 25 | pipes = set() 26 | 27 | class GetSrc: 28 | def __init__(self, username=None, token=None, url=None, repo=None, num=10, target=None, timeout=3, signame=None, mirror=None, jar_suffix=None): 29 | self.jar_suffix = jar_suffix if jar_suffix else 'jar' 30 | self.mirror = int(str(mirror).strip()) if mirror else 1 31 | self.registry = 'github.com' 32 | self.mirror_proxy = 'https://ghp.ci/https://raw.githubusercontent.com' 33 | self.num = int(num) 34 | self.sep = os.path.sep 35 | self.username = username 36 | self.token = token 37 | self.timeout=timeout 38 | self.url = url 39 | self.repo = repo if repo else 'tvbox' 40 | self.target = f'{target.split(".json")[0]}.json' if target else 'tvbox.json' 41 | self.headers = {"user-agent": "okhttp/3.15 Html5Plus/1.0 (Immersed/23.92157)"} 42 | self.s = requests.Session() 43 | self.signame = signame 44 | retries = Retry(total=3, backoff_factor=1) 45 | self.s.mount('http://', HTTPAdapter(max_retries=retries)) 46 | self.s.mount('https://', HTTPAdapter(max_retries=retries)) 47 | self.size_tolerance = 15 # 线路文件大小误差在15以内认为是同一个 48 | self.main_branch = 'main' 49 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 50 | self.gh1 = [ 51 | 'https://ghp.ci/https://raw.githubusercontent.com', 52 | 'https://gitdl.cn/https://raw.githubusercontent.com', 53 | 'https://ghproxy.net/https://raw.githubusercontent.com', 54 | 'https://github.moeyy.xyz/https://raw.githubusercontent.com', 55 | 'https://gh-proxy.com/https://raw.githubusercontent.com', 56 | 'https://ghproxy.cc/https://raw.githubusercontent.com', 57 | 'https://raw.yzuu.cf', 58 | 'https://raw.nuaa.cf', 59 | 'https://raw.kkgithub.com', 60 | 'https://mirror.ghproxy.com/https://raw.githubusercontent.com', 61 | 'https://gh.llkk.cc/https://raw.githubusercontent.com', 62 | 'https://gh.ddlc.top/https://raw.githubusercontent.com', 63 | 'https://gh-proxy.llyke.com/https://raw.githubusercontent.com', 64 | 'https://slink.ltd', 65 | 'https://cors.zme.ink', 66 | 'https://git.886.be' 67 | ] 68 | self.gh2 = [ 69 | "https://fastly.jsdelivr.net/gh", 70 | "https://jsd.onmicrosoft.cn/gh", 71 | "https://gcore.jsdelivr.net/gh", 72 | "https://cdn.jsdmirror.com/gh", 73 | "https://cdn.jsdmirror.cn/gh", 74 | "https://jsd.proxy.aks.moe/gh", 75 | "https://jsdelivr.b-cdn.net/gh", 76 | "https://jsdelivr.pai233.top/gh" 77 | ] 78 | def file_hash(self, filepath): 79 | with open(filepath, 'rb') as f: 80 | file_contents = f.read() 81 | return hashlib.sha256(file_contents).hexdigest() 82 | def remove_duplicates(self, folder_path): 83 | folder_path = Path(folder_path) 84 | jar_folder = f'{folder_path}/jar' 85 | excludes = {'.json', '.git', 'jar', '.idea', 'ext', '.DS_Store', '.md'} 86 | files_info = {} 87 | 88 | # 把jar目录下所有文件后缀都改成新的self.jar_suffix 89 | self.rename_jar_suffix(jar_folder) 90 | 91 | # 存储文件名、大小和哈希值 92 | for file_path in folder_path.iterdir(): 93 | if file_path.is_file() and file_path.suffix not in excludes: 94 | file_size = file_path.stat().st_size 95 | file_hash = self.file_hash(file_path) 96 | files_info[file_path.name] = {'path': str(file_path), 'size': file_size, 'hash': file_hash} 97 | 98 | # 保留的文件列表 99 | keep_files = [] 100 | # 按文件大小排序,然后按顺序处理 101 | for file_name, info in sorted(files_info.items(), key=lambda item: item[1]['size']): 102 | if not keep_files or abs(info['size'] - files_info[keep_files[-1]]['size']) > self.size_tolerance: 103 | keep_files.append(file_name) 104 | # 删除jar目录下除了{self.jar_suffix}的文件 105 | self.remove_all_except_jar(jar_folder) 106 | else: 107 | # 如果当前文件大小在容忍范围内,删除当前文件和对应的jar文件 108 | os.remove(info['path']) 109 | self.remove_jar_file(jar_folder, file_name.replace('.txt', f'{self.jar_suffix}')) 110 | 111 | keep_files.sort() 112 | return keep_files 113 | def rename_jar_suffix(self,jar_folder): 114 | # 遍历目录中的所有文件和子目录 115 | for root, dirs, files in os.walk(jar_folder): 116 | for file in files: 117 | # 构造完整的文件路径 118 | old_file = os.path.join(root, file) 119 | # 构造新的文件名,去除原有的后缀,加上 self.jar_suffix 120 | new_file = os.path.join(root, os.path.splitext(file)[0] + f'.{self.jar_suffix}') 121 | # 重命名文件 122 | os.rename(old_file, new_file) 123 | # print(f"文件已重命名: {old_file} -> {new_file}") 124 | def remove_all_except_jar(self, jar_folder): 125 | # 列出文件夹中的所有文件 126 | for file_name in os.listdir(jar_folder): 127 | # 构建完整的文件路径 128 | full_path = os.path.join(jar_folder, file_name) 129 | # 检查是否为文件 130 | if os.path.isfile(full_path): 131 | # 获取文件的扩展名 132 | _, file_extension = os.path.splitext(file_name) 133 | # 如果扩展名不是self.jar_suffix,则删除文件 134 | if file_extension != f'.{self.jar_suffix}': 135 | self.remove_jar_file(jar_folder, file_name) 136 | def remove_jar_file(self, jar_folder, file_name): 137 | # 构建jar文件的路径 138 | jar_file_path = os.path.join(jar_folder, file_name) 139 | # 如果jar文件存在,则删除它 140 | if os.path.isfile(jar_file_path): 141 | os.remove(jar_file_path) 142 | def remove_emojis(self, text): 143 | emoji_pattern = re.compile("[" 144 | u"\U0001F600-\U0001F64F" # emoticons 145 | u"\U0001F300-\U0001F5FF" # symbols & pictographs 146 | u"\U0001F680-\U0001F6FF" # transport & map symbols 147 | u"\U0001F1E0-\U0001F1FF" # flags (iOS) 148 | "\U00002500-\U00002BEF" # chinese char 149 | "\U00010000-\U0010ffff" 150 | "\u200d" # zero width joiner 151 | "\u20E3" # combining enclosing keycap 152 | "\ufe0f" # VARIATION SELECTOR-16 153 | "]+", flags=re.UNICODE) 154 | text = text.replace('/', '_').replace('多多', '').replace('┃', '').replace('线路', '').replace('匚','').strip() 155 | return emoji_pattern.sub('', text) 156 | def json_compatible(self, str): 157 | # 兼容错误json 158 | res = str.replace(' ', '').replace("'",'"').replace('key:', '"key":').replace('name:', '"name":').replace('type:', '"type":').replace('api:','"api":').replace('searchable:', '"searchable":').replace('quickSearch:', '"quickSearch":').replace('filterable:','"filterable":').strip() 159 | return res 160 | def ghproxy(self, str): 161 | u = 'https://ghp.ci/' 162 | res = str.replace('https://ghproxy.net/', u).replace('https://ghproxy.com/', u).replace('https://gh-proxy.com/',u).replace('https://mirror.ghproxy.com/',u) 163 | return res 164 | def set_hosts(self): 165 | # 设置github.com的加速hosts 166 | try: 167 | response = requests.get('https://hosts.gitcdn.top/hosts.json') 168 | if response.status_code == 200: 169 | hosts_data = response.json() 170 | # 遍历JSON数据,找到"github.com"对应的IP 171 | github_ip = None 172 | for entry in hosts_data: 173 | if entry[1] == "github.com": 174 | github_ip = entry[0] 175 | break 176 | if github_ip: 177 | # 读取现有的/etc/hosts文件 178 | with open('/etc/hosts', 'r+') as file: 179 | hosts_content = file.read() 180 | # 检查是否已经存在对应的IP 181 | if github_ip not in hosts_content: 182 | # 将新的IP添加到文件末尾 183 | file.write(f'\n{github_ip} github.com') 184 | print(f'IP address {github_ip} for github.com has been added to /etc/hosts.') 185 | else: 186 | print(f'IP address for github.com is already in /etc/hosts.') 187 | else: 188 | print('No IP found for github.com in the provided data.') 189 | else: 190 | print('Failed to retrieve data from https://hosts.gitcdn.top/hosts.json') 191 | except Exception as e: 192 | pass 193 | def picparse(self, url): 194 | r = self.s.get(url, headers=self.headers, timeout=self.timeout, verify=False) 195 | pattern = r'([A-Za-z0-9+/]+={0,2})' 196 | matches = re.findall(pattern, r.text) 197 | decoded_data = base64.b64decode(matches[-1]) 198 | text = decoded_data.decode('utf-8') 199 | return text 200 | def js_render(self, url): 201 | # 获取js渲染页面源代码 202 | timeout = self.timeout * 4 203 | if timeout > 15: 204 | timeout = 15 205 | browser_args = ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer','--disable-setuid-sandbox'] 206 | session = HTMLSession(browser_args=browser_args) 207 | r = session.get(f'http://lige.unaux.com/?url={url}', headers=self.headers, timeout=timeout, verify=False) 208 | # 等待页面加载完成,Requests-HTML 会自动等待 JavaScript 执行完成 209 | r.html.render(timeout=timeout) 210 | # print('解密结果:',r.html.text) 211 | return r.html 212 | def get_jar(self, name, url, text): 213 | name = f'{name}.{self.jar_suffix}' 214 | pattern = r'\"spider\":(\s)?\"([^,]+)\"' 215 | matches = re.search(pattern, text) 216 | try: 217 | jar = matches.group(2).replace('./', f'{url}/').split(';')[0] 218 | jar = jar.split('"spider":"')[-1] 219 | if name==f'{self.repo}.{self.jar_suffix}': 220 | name = f"{jar.split('/')[-1]}" 221 | # print('jar地址: ', jar) 222 | timeout = self.timeout * 4 223 | if timeout > 15: 224 | timeout = 15 225 | r = self.s.get(jar, timeout=timeout) 226 | with open(f'{self.repo}/jar/{name}', 'wb') as f: 227 | f.write(r.content) 228 | jar = f'{self.slot}/jar/{name}' 229 | print(111111,jar) 230 | text = text.replace(matches.group(2), jar) 231 | except Exception as e: 232 | print(f'【jar下载失败】{name} jar地址: {jar} error:{e}') 233 | return text 234 | def download(self, url, name, filename, cang=True): 235 | # 下载单线路 236 | item = {} 237 | try: 238 | path = os.path.dirname(url) 239 | r = self.s.get(url, headers=self.headers, allow_redirects=True, timeout=self.timeout, verify=False) 240 | if r.status_code == 200: 241 | print("开始下载【线路】{}: {}".format(name, url)) 242 | if 'searchable' not in r.text: 243 | r = self.js_render(url) 244 | if not r.text: 245 | r = self.picparse(url) 246 | if 'searchable' not in r: 247 | raise 248 | r = self.get_jar(name, url, r) 249 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f: 250 | f.write(r) 251 | return 252 | if 'searchable' not in r.text: 253 | raise 254 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f: 255 | try: 256 | if r.content.decode('utf8').startswith(u'\ufeff'): 257 | str = r.content.decode('utf8').encode('utf-8')[3:].decode('utf-8') 258 | else: 259 | str = r.content.decode('utf-8').replace('./', f'{path}/') 260 | except: 261 | str = r.text 262 | finally: 263 | r = self.ghproxy(str.replace('./', f'{path}/')) 264 | 265 | r = self.get_jar(name, url, r) 266 | f.write(r) 267 | pipes.add(name) 268 | 269 | except Exception as e: 270 | print(f"【线路】{name}: {url} 下载错误:{e}") 271 | # 单仓时写入item 272 | if os.path.exists(f'{self.repo}{self.sep}{filename}') and cang: 273 | item['name'] = name 274 | item['url'] = f'{self.slot}/{filename}' 275 | items.append(item) 276 | def down(self, data, s_name): 277 | ''' 278 | 下载单仓 279 | ''' 280 | newJson = {} 281 | global items 282 | items = [] 283 | urls = data.get("urls") if data.get("urls") else data.get("sites") 284 | for u in urls: 285 | name = u.get("name").strip() 286 | name = self.remove_emojis(name) 287 | url = u.get("url") 288 | url = self.ghproxy(url) 289 | filename = '{}.txt'.format(name) 290 | if name in pipes: 291 | print(f"【线路】{name} 已存在,无需重复下载") 292 | continue 293 | self.download(url, name, filename) 294 | newJson['urls'] = items 295 | newJson = pprint.pformat(newJson, width=200) 296 | print(f'开始写入单仓{s_name}') 297 | with open(f'{self.repo}{self.sep}{s_name}', 'w+', encoding='utf-8') as f: 298 | content = str(newJson).replace("'", '"') 299 | f.write(json.loads(json.dumps(content, indent=4, ensure_ascii=False))) 300 | def all(self): 301 | # 整合所有文件到all.json 302 | newJson = {} 303 | items = [] 304 | files = self.remove_duplicates(self.repo) 305 | for file in files: 306 | item = {} 307 | item['name'] = file.split('.txt')[0] 308 | item['url'] = f'{self.slot}/{file}' 309 | items.append(item) 310 | newJson['urls'] = items 311 | newJson = pprint.pformat(newJson, width=200) 312 | print(f'开始写入all.json') 313 | with open(f'{self.repo}{self.sep}all.json', 'w+', encoding='utf-8') as f: 314 | content = str(newJson).replace("'", '"') 315 | f.write(json.loads(json.dumps(content, indent=4, ensure_ascii=False))) 316 | def batch_handle_online_interface(self): 317 | # 下载线路,处理多url场景 318 | print(f'--------- 开始私有化在线接口 ----------') 319 | urls = self.url.split(',') 320 | for url in urls: 321 | item = url.split('?&signame=') 322 | self.url = item[0] 323 | self.signame = item[1] if len(item) > 1 else None 324 | print(f'当前url: {self.url}') 325 | self.storeHouse() 326 | def git_clone(self): 327 | # self.registry = 'githubfast.com' 328 | # self.registry = 'hub.yzuu.cf' 329 | print(f'开始克隆:git clone https://github.com/{self.username}/{self.repo}.git') 330 | self.domain = f'https://{self.token}@{self.registry}/{self.username}/{self.repo}.git' 331 | if os.path.exists(self.repo): 332 | subprocess.call(['rm', '-rf', self.repo]) 333 | try: 334 | repo = git.Repo.clone_from(self.domain, to_path=self.repo, depth=1) 335 | [os.makedirs(d, exist_ok=True) for d in [f'{self.repo}/jar']] 336 | self.get_local_repo() 337 | except Exception as e: 338 | try: 339 | self.registry = 'gitdl.cn' 340 | self.domain = f'https://{self.token}@{self.registry}/https://github.com/{self.username}/{self.repo}.git' 341 | if os.path.exists(self.repo): 342 | subprocess.call(['rm', '-rf', self.repo]) 343 | repo = git.Repo.clone_from(self.domain, to_path=self.repo, depth=1) 344 | [os.makedirs(d, exist_ok=True) for d in [f'{self.repo}/jar']] 345 | self.get_local_repo() 346 | except Exception as e: 347 | print(222222, e) 348 | def get_local_repo(self): 349 | # 打开本地仓库,读取仓库信息 350 | repo = git.Repo(self.repo) 351 | config_writer = repo.config_writer() 352 | config_writer.set_value('user', 'name', self.username) 353 | config_writer.set_value('user', 'email', self.username) 354 | # 设置 http.postBuffer 355 | config_writer.set_value('http', 'postBuffer', '524288000') 356 | config_writer.release() 357 | 358 | # 获取远程仓库的引用 359 | remote = repo.remote(name='origin') 360 | # 获取远程分支列表 361 | remote_branches = remote.refs 362 | # 遍历远程分支,查找主分支 363 | for branch in remote_branches: 364 | if branch.name == 'origin/master' or branch.name == 'origin/main': 365 | self.main_branch = branch.name.split('/')[-1] 366 | break 367 | print(f"仓库{self.repo} 主分支为: {self.main_branch}") 368 | self.mirror_init() 369 | return repo 370 | def git_push(self,repo): 371 | # 推送并重置commit计数 372 | # 推送 373 | print(f'--------- 完成私有化在线接口 ----------\n开始推送:git push https://{self.registry}/{self.username}/{self.repo}.git') 374 | try: 375 | repo.git.add(A=True) 376 | repo.git.commit(m="update") 377 | repo.git.push() 378 | except Exception as e: 379 | # print('git推送异常', e) 380 | # 打开本地仓库,读取仓库信息 381 | config_writer = repo.config_writer() 382 | self.registry = 'github.com' 383 | config_writer.set_value('remote "origin"', 'url', f'https://{self.token}@{self.registry}/{self.username}/{self.repo}.git') 384 | config_writer.release() 385 | # 重置commit 386 | try: 387 | os.chdir(self.repo) 388 | # print('开始清理git',os.getcwd()) 389 | repo.git.checkout('--orphan', 'tmp_branch') 390 | repo.git.add(A=True) 391 | repo.git.commit(m="update") 392 | repo.git.execute(['git', 'branch', '-D', self.main_branch]) 393 | repo.git.execute(['git', 'branch', '-m', self.main_branch]) 394 | repo.git.execute(['git', 'push', '-f', 'origin', self.main_branch]) 395 | except Exception as e: 396 | print('git清理异常', e) 397 | def storeHouse(self): 398 | ''' 399 | 生成多仓json文件 400 | ''' 401 | newJson = {} 402 | items = [] 403 | # 解析最初链接 404 | try: 405 | res = self.s.get(self.url, headers=self.headers, verify=False).content.decode('utf8') 406 | except Exception as e: 407 | res = self.js_render(self.url).text.replace(' ', '').replace("'", '"') 408 | if not res: 409 | res = self.picparse(self.url).replace(' ', '').replace("'", '"') 410 | # 线路 411 | if 'searchable' in str(res): 412 | filename = self.signame + '.txt' if self.signame else f"{''.join(random.choices(string.ascii_letters + string.digits, k=10))}.txt" 413 | path = os.path.dirname(self.url) 414 | print("【线路】 {}: {}".format(self.repo, self.url)) 415 | try: 416 | with open(f'{self.repo}{self.sep}{filename}', 'w+', encoding='utf-8') as f, open( 417 | f'{self.repo}{self.sep}{self.target}', 'w+', encoding='utf-8') as f2: 418 | r = self.ghproxy(res.replace('./', f'{path}/')) 419 | r = self.get_jar(filename.split('.txt')[0], url, r) 420 | f.write(r) 421 | f2.write(r) 422 | except Exception as e: 423 | print(333333333, e) 424 | return 425 | 426 | # json容错处理 427 | res = self.json_compatible(res) 428 | # 移除注释 429 | datas = '' 430 | for d in res.splitlines(): 431 | if d.find(" //") != -1 or d.find("// ") != -1 or d.find(",//") != -1 or d.startswith("//"): 432 | d = d.split(" //", maxsplit=1)[0] 433 | d = d.split("// ", maxsplit=1)[0] 434 | d = d.split(",//", maxsplit=1)[0] 435 | d = d.split("//", maxsplit=1)[0] 436 | datas = '\n'.join([datas, d]) 437 | # 容错处理,便于json解析 438 | datas = datas.replace('\n', '') 439 | res = datas.replace(' ', '').replace("'", '"').replace('\n', '') 440 | if datas.startswith(u'\ufeff'): 441 | try: 442 | res = datas.encode('utf-8')[3:].decode('utf-8').replace(' ', '').replace("'", '"').replace('\n', '') 443 | except Exception as e: 444 | res = datas.encode('utf-8')[4:].decode('utf-8').replace(' ', '').replace("'", '"').replace('\n', '') 445 | 446 | # 多仓 447 | elif 'storeHouse' in datas: 448 | res = json.loads(str(res)) 449 | srcs = res.get("storeHouse") if res.get("storeHouse") else None 450 | if srcs: 451 | i = 1 452 | for s in srcs: 453 | if i > self.num: 454 | break 455 | i += 1 456 | item = {} 457 | s_name = s.get("sourceName") 458 | s_name = self.remove_emojis(s_name) 459 | s_name = f'{s_name}.json' 460 | s_url = s.get("sourceUrl") 461 | print("【多仓】 {}: {}".format(s_name, s_url)) 462 | try: 463 | if self.s.get(s_url, headers=self.headers).status_code >= 400: 464 | continue 465 | except Exception as e: 466 | print('地址无法响应: ',e) 467 | continue 468 | try: 469 | if self.s.get(s_url, headers=self.headers).content.decode('utf8').lstrip().startswith(u'\ufeff'): 470 | data = self.s.get(s_url, headers=self.headers).content.decode('utf-8')[1:] 471 | else: 472 | data = self.s.get(s_url, headers=self.headers).content.decode('utf-8') 473 | except Exception as e: 474 | try: 475 | data = self.s.get(s_url, headers=self.headers).content.decode('utf8') 476 | data = data.encode('utf-8').decode('utf-8') 477 | except Exception as e: 478 | continue 479 | datas = '' 480 | for d in data.splitlines(): 481 | if d.find(" //") != -1 or d.find("// ") != -1 or d.find(",//") != -1 or d.startswith("//"): 482 | d = d.split(" //", maxsplit=1)[0] 483 | d = d.split("// ", maxsplit=1)[0] 484 | d = d.split(",//", maxsplit=1)[0] 485 | d = d.split("//", maxsplit=1)[0] 486 | datas = '\n'.join([datas, d]) 487 | 488 | try: 489 | if datas.lstrip().startswith(u'\ufeff'): 490 | datas = datas.encode('utf-8')[1:] 491 | self.down(json.loads(datas), s_name) 492 | except Exception as e: 493 | try: 494 | data = self.s.get(s_url, headers=self.headers).text 495 | except Exception as e: 496 | continue 497 | datas = '' 498 | for d in data.splitlines(): 499 | datas += d.replace('\n', '').replace(' ', '').strip() 500 | datas = datas.encode('utf-8') 501 | if 'DOCTYPEhtml' in str(datas): 502 | continue 503 | datas = re.sub(r'^(.*?)\{', '{', datas.decode('utf-8'), flags=re.DOTALL | re.MULTILINE) 504 | self.down(json.loads(datas), s_name) 505 | item['sourceName'] = s_name.split('.json')[0] 506 | item['sourceUrl'] = f'{self.slot}/{s_name}' 507 | items.append(item) 508 | newJson["storeHouse"] = items 509 | newJson = pprint.pformat(newJson, width=200) 510 | with open(f'{self.repo}{self.sep}{self.target}', 'w+', encoding='utf-8') as f: 511 | print(f"开始写入{self.target}") 512 | f.write(json.dumps(json.loads(str(newJson).replace("'", '"')), sort_keys=True, indent=4, ensure_ascii=False)) 513 | # 单仓 514 | else: 515 | try: 516 | res = json.loads(str(res)) 517 | except Exception as e: 518 | res = self.js_render(self.url).text.replace(' ', '').replace("'", '"') 519 | if not res: 520 | res = self.picparse(self.url).replace(' ', '').replace("'", '"') 521 | try: 522 | res = json.loads(str(res)) 523 | except Exception as e: 524 | # print(111111, e, res) 525 | pass 526 | s_name = self.target 527 | s_url = self.url 528 | print("【单仓】 {}: {}".format(s_name, s_url)) 529 | try: 530 | self.down(res, s_name) 531 | except Exception as e: 532 | if 'searchable' in str(res): 533 | filename = self.signame + '.txt' if self.signame else f"{''.join(random.choices(string.ascii_letters + string.digits, k=10))}.txt" 534 | print("【线路】 {}: {}".format(filename, self.url)) 535 | try: 536 | self.download(self.url, filename.split('.txt')[0], filename, cang=False) 537 | except Exception as e: 538 | print('下载异常', e) 539 | def replace_urls_gh1(self, content): 540 | # 适用于https://ghp.ci/https://raw.githubusercontent.com|https://raw.yzuu.cf类型 541 | # 使用正则表达式查找并替换链接 542 | def replace_match(match): 543 | username = match.group(2) 544 | repo_name = match.group(3) 545 | path = match.group(4) 546 | # 构建新的URL 547 | new_url = f"{self.mirror_proxy}/{username}/{repo_name}/{self.main_branch}" 548 | if path: 549 | new_url += path 550 | return new_url 551 | return self.pattern.sub(replace_match, content) 552 | def replace_urls_gh2(self, content): 553 | # 适用于https://gcore/jsdelivr.net/gh类型 554 | # 替换函数 555 | def replace_match(match): 556 | return f'{self.mirror_proxy}/{match.group(2)}/{match.group(3)}{match.group(5)}' 557 | # 执行替换 558 | return self.pattern.sub(replace_match, content) 559 | def mirror_proxy2new(self): 560 | # 把文本文件中所有镜像代理路径替换掉 561 | if self.mirror < 20: 562 | # gh1 适用于https://ghp.ci/https://raw.githubusercontent.com|https://raw.yzuu.cf类型 563 | # 自动转换提供的域名列表为正则表达式所需的格式 564 | patterns = [re.escape(proxy) for proxy in self.gh2] 565 | # 组合成一个正则表达式 566 | self.pattern = re.compile(r'({})/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_-]+)(/.*)?'.format('|'.join(patterns))) 567 | # 遍历文件夹中的所有文件 568 | for root, dirs, files in os.walk(self.repo): 569 | for file in files: 570 | if file.endswith('.txt') or file.endswith('.json'): 571 | file_path = os.path.join(root, file) 572 | with open(file_path, 'r', encoding='utf-8') as f: 573 | content = f.read() 574 | # 替换文件中的URL 575 | new_content = self.replace_urls_gh1(content) 576 | for i in self.gh1: 577 | new_content = new_content.replace(i,self.mirror_proxy) 578 | with open(file_path, 'w', encoding='utf-8') as f: 579 | f.write(new_content) 580 | elif self.mirror > 20: 581 | # gh2适用于https://gcore.jsdelivr.net/gh类型 582 | if self.jar_suffix not in ['html','js','css','json','txt']: 583 | return 584 | # 自动转换提供的域名列表为正则表达式所需的格式 585 | patterns = [re.escape(proxy) for proxy in self.gh1] 586 | # 组合成一个正则表达式 587 | self.pattern = re.compile(r'({})/(.+?)/(.+?)/(master|main)(/|/.*)'.format('|'.join(patterns))) 588 | # 遍历文件夹中的所有文件 589 | for filename in os.listdir(self.repo): 590 | file_path = os.path.join(self.repo, filename) 591 | # 检查是否是文件而不是文件夹 592 | if os.path.isfile(file_path) and (filename.endswith('.txt') or filename.endswith('.json')): 593 | # 读取文件内容 594 | with open(file_path, 'r', encoding='utf-8') as file: 595 | content = file.read() 596 | # 替换文件中的URL 597 | new_content = self.replace_urls_gh2(content) 598 | for i in self.gh2: 599 | new_content = new_content.replace(i,self.mirror_proxy) 600 | # 写回文件 601 | with open(file_path, 'w', encoding='utf-8') as f: 602 | f.write(new_content) 603 | def mirror_init(self): 604 | # gh1 605 | if self.mirror == 1: 606 | self.mirror_proxy = 'https://ghp.ci/https://raw.githubusercontent.com' 607 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 608 | elif self.mirror == 2: 609 | self.mirror_proxy = 'https://gitdl.cn/https://raw.githubusercontent.com' 610 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 611 | elif self.mirror == 3: 612 | self.mirror_proxy = 'https://ghproxy.net/https://raw.githubusercontent.com' 613 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 614 | elif self.mirror == 4: 615 | self.mirror_proxy = 'https://github.moeyy.xyz/https://raw.githubusercontent.com' 616 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 617 | elif self.mirror == 5: 618 | self.mirror_proxy = 'https://gh-proxy.com/https://raw.githubusercontent.com' 619 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 620 | elif self.mirror == 6: 621 | self.mirror_proxy = 'https://ghproxy.cc/https://raw.githubusercontent.com' 622 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 623 | elif self.mirror == 7: 624 | self.mirror_proxy = 'https://raw.yzuu.cf' 625 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 626 | # self.registry = 'hub.yzuu.cf' 627 | elif self.mirror == 8: 628 | self.mirror_proxy = 'https://raw.nuaa.cf' 629 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 630 | elif self.mirror == 9: 631 | self.mirror_proxy = 'https://raw.kkgithub.com' 632 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}/{self.main_branch}' 633 | elif self.mirror == 10: 634 | self.mirror_proxy = 'https://gh.con.sh/https://raw.githubusercontent.com' 635 | elif self.mirror == 11: 636 | self.mirror_proxy = 'https://gh.llkk.cc/https://raw.githubusercontent.com' 637 | elif self.mirror == 12: 638 | self.mirror_proxy = 'https://gh.ddlc.top/https://raw.githubusercontent.com' 639 | elif self.mirror == 13: 640 | self.mirror_proxy = 'https://gh-proxy.llyke.com/https://raw.githubusercontent.com' 641 | 642 | # gh2 643 | elif self.mirror == 21: 644 | self.mirror_proxy = "https://fastly.jsdelivr.net/gh" 645 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 646 | elif self.mirror == 22: 647 | self.mirror_proxy = "https://jsd.onmicrosoft.cn/gh" 648 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 649 | elif self.mirror == 23: 650 | self.mirror_proxy = "https://gcore.jsdelivr.net/gh" 651 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 652 | elif self.mirror == 24: 653 | self.mirror_proxy = "https://cdn.jsdmirror.com/gh" 654 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 655 | elif self.mirror == 25: 656 | self.mirror_proxy = "https://cdn.jsdmirror.cn/gh" 657 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 658 | elif self.mirror == 26: 659 | self.mirror_proxy = "https://jsd.proxy.aks.moe/gh" 660 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 661 | elif self.mirror == 27: 662 | self.mirror_proxy = "https://jsdelivr.b-cdn.net/gh" 663 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 664 | elif self.mirror == 28: 665 | self.mirror_proxy = "https://jsdelivr.pai233.top/gh" 666 | self.slot = f'{self.mirror_proxy}/{self.username}/{self.repo}' 667 | 668 | def run(self): 669 | start_time = time.time() 670 | self.set_hosts() 671 | self.mirror_init() 672 | self.git_clone() 673 | self.batch_handle_online_interface() 674 | repo = self.get_local_repo() 675 | self.all() 676 | self.mirror_proxy2new() 677 | self.git_push(repo) 678 | end_time = time.time() 679 | print(f'耗时: {end_time - start_time} 秒\n\n#################影视仓APP配置接口########################\n\n{self.slot}/all.json\n{self.slot}/{self.target}') 680 | 681 | 682 | if __name__ == '__main__': 683 | token = os.getenv('token') 684 | username = os.getenv('username') if os.getenv('username') else os.getenv('u') 685 | repo = os.getenv('repo') 686 | signame = os.getenv('signame') 687 | target = os.getenv('target') 688 | num = os.getenv('num') if os.getenv('num') else 10 689 | url = os.getenv('url') 690 | timeout = os.getenv('timeout') if os.getenv('timeout') else 3 691 | mirror = os.getenv('mirror') 692 | jar_suffix = os.getenv('jar_suffix') 693 | GetSrc(username=username, token=token, url=url, repo=repo, num=num, target=target, timeout=timeout, signame=signame, mirror=mirror, jar_suffix=jar_suffix).run() 694 | --------------------------------------------------------------------------------