1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-03-21 09:09:45 +08:00

支持代理 m3u8 格式链接

This commit is contained in:
涵曦
2026-01-19 16:03:26 +08:00
parent ddb3e98f16
commit 605e5d53b9

View File

@@ -54,6 +54,50 @@ from xiaomusic.utils.system_utils import try_add_access_control_param
router = APIRouter()
def _process_m3u8_content(m3u8_content: str, base_url: str, radio: bool = None) -> str:
"""处理 m3u8 文件内容,将资源 URL 替换为代理 URL
Args:
m3u8_content: m3u8 文件内容
base_url: m3u8 文件的 URL用于解析相对路径
radio: 是否为电台直播流
Returns:
str: 处理后的 m3u8 内容
"""
from urllib.parse import urljoin
lines = m3u8_content.split('\n')
processed_lines = []
for line in lines:
stripped_line = line.strip()
# 跳过注释行和空行
if not stripped_line or stripped_line.startswith('#'):
processed_lines.append(line)
continue
# 处理资源行(.ts、.m3u8 等)
# 判断是否为 URL包含协议或以 / 开头)
if stripped_line.startswith(('http://', 'https://', '/')):
# 绝对 URL直接使用
resource_url = stripped_line
else:
# 相对 URL需要拼接
resource_url = urljoin(base_url, stripped_line)
# 将资源 URL 替换为代理 URL
urlb64 = base64.b64encode(resource_url.encode("utf-8")).decode("utf-8")
proxy_url = f"/proxy?urlb64={urlb64}"
if radio is not None:
proxy_url += f"&radio={'true' if radio else 'false'}"
processed_lines.append(proxy_url)
return '\n'.join(processed_lines)
@router.post("/api/file/cleantempdir")
async def cleantempdir(Verifcation=Depends(verification)):
await clean_temp_dir(xiaomusic.config)
@@ -365,7 +409,50 @@ async def proxy(urlb64: str, radio: bool = None):
status_code=resp.status, detail=f"下载失败,状态码: {resp.status}"
) from status_exc
# 流式生成器与download_file的分块逻辑一致
# 提取文件名根据URL扩展名智能判断
filename = parsed_url.path.split("/")[-1].split("?")[0]
content_type = resp.headers.get("Content-Type", "").lower()
# 判断是否为 m3u8 文件
is_m3u8 = (
url.lower().endswith(".m3u8")
or "mpegurl" in content_type
or "m3u8" in content_type
)
if not filename:
# 根据URL扩展名或Content-Type设置默认文件名
path_lower = parsed_url.path.lower()
if path_lower.endswith(".m3u8") or is_m3u8:
filename = "stream.m3u8"
elif path_lower.endswith(".m3u"):
filename = "stream.m3u"
else:
filename = "output.mp3"
# 如果是 m3u8 文件,需要处理内容,将相对路径替换为代理 URL
if is_m3u8:
try:
# 读取完整的 m3u8 内容
m3u8_content = await resp.text()
await close_session()
# 处理 m3u8 内容,替换资源 URL
processed_content = _process_m3u8_content(m3u8_content, url, radio)
# 返回处理后的内容
return Response(
content=processed_content,
media_type="application/vnd.apple.mpegurl",
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
except Exception as e:
log.exception(f"处理 m3u8 文件失败: {e}")
# 失败时返回原始内容
await close_session()
raise
# 非 m3u8 文件,使用流式传输
async def stream_generator():
try:
async for data in resp.content.iter_chunked(4096):
@@ -373,23 +460,6 @@ async def proxy(urlb64: str, radio: bool = None):
finally:
await close_session()
# 提取文件名根据URL扩展名智能判断
filename = parsed_url.path.split("/")[-1].split("?")[0]
if not filename:
# 根据URL扩展名或Content-Type设置默认文件名
path_lower = parsed_url.path.lower()
if path_lower.endswith(".m3u8"):
filename = "stream.m3u8"
elif path_lower.endswith(".m3u"):
filename = "stream.m3u"
else:
# 根据Content-Type推断
content_type = resp.headers.get("Content-Type", "").lower()
if "mpegurl" in content_type or "m3u8" in content_type:
filename = "stream.m3u8"
else:
filename = "output.mp3"
return StreamingResponse(
stream_generator(),
media_type=resp.headers.get("Content-Type", "audio/mpeg"),