Skip to content

Commit 15a2c83

Browse files
feat(Alist2Strm): 添加 BDMV 蓝光原盘支持 #133
1 parent 21f6e03 commit 15a2c83

1 file changed

Lines changed: 182 additions & 1 deletion

File tree

app/modules/alist2strm/alist2strm.py

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from os import PathLike
33
from pathlib import Path
44
from re import compile as re_compile
5+
import traceback
56

67
from aiofile import async_open
78

@@ -94,6 +95,10 @@ async def run(self) -> None:
9495
"""
9596
处理主体
9697
"""
98+
99+
# BDMV 处理相关变量初始化
100+
self.bdmv_collections: dict[str, list[tuple[AlistPath, int]]] = {} # BDMV目录 -> [(文件路径, 文件大小)]
101+
self.bdmv_largest_files: dict[str, AlistPath] = {} # BDMV目录 -> 最大文件路径
97102

98103
def filter(path: AlistPath) -> bool:
99104
"""
@@ -107,10 +112,25 @@ def filter(path: AlistPath) -> bool:
107112
if path.is_dir:
108113
return False
109114

115+
# 跳过系统文件夹和不需要的文件
116+
if any(folder in path.full_path for folder in ["@eaDir", "Thumbs.db", ".DS_Store"]):
117+
return False
118+
119+
# 完全跳过 BDMV 文件夹内的所有文件(除了我们特殊处理的 .m2ts 文件)
120+
if "/BDMV/" in path.full_path and not self._is_bdmv_file(path):
121+
logger.debug(f"跳过 BDMV 文件夹内的文件: {path.name}")
122+
return False
123+
110124
if path.suffix.lower() not in self.process_file_exts:
111125
logger.debug(f"文件 {path.name} 不在处理列表中")
112126
return False
113127

128+
# 检查是否为 BDMV 文件
129+
if self._is_bdmv_file(path):
130+
self._collect_bdmv_file(path)
131+
# 暂时不处理,等收集完所有文件后再决定
132+
return False
133+
114134
try:
115135
local_path = self.__get_local_path(path)
116136
except OSError as e: # 可能是文件名过长
@@ -152,15 +172,52 @@ def filter(path: AlistPath) -> bool:
152172

153173
self.processed_local_paths = set() # 云盘文件对应的本地文件路径
154174

175+
# 第一阶段:收集所有文件信息并直接处理普通文件
155176
async with self.__max_workers, TaskGroup() as tg:
156177
async for path in self.client.iter_path(
157178
dir_path=self.source_dir,
158179
wait_time=self.wait_time,
159180
is_detail=is_detail,
160181
filter=filter,
161182
):
183+
# 直接处理普通文件,不需要额外的 list
162184
tg.create_task(self.__file_processer(path))
163185

186+
# 完成 BDMV 文件收集,确定最大文件
187+
self._finalize_bdmv_collections()
188+
189+
# 第二阶段:处理 BDMV 最大文件
190+
logger.info(f"开始处理 {len(self.bdmv_largest_files)} 个 BDMV 目录")
191+
for bdmv_root, largest_file in self.bdmv_largest_files.items():
192+
try:
193+
logger.info(f"处理 BDMV 目录: {bdmv_root}")
194+
logger.info(f"最大文件: {largest_file.full_path}")
195+
196+
# 重新获取详细信息以确保有 raw_url
197+
if self.mode == "RawURL" and not largest_file.raw_url:
198+
logger.debug(f"重新获取 BDMV 文件详细信息: {largest_file.full_path}")
199+
try:
200+
updated_path = await self.client.async_api_fs_get(largest_file.full_path)
201+
# 保持原有的 full_path,只更新其他属性
202+
original_full_path = largest_file.full_path
203+
largest_file = updated_path
204+
largest_file.full_path = original_full_path
205+
except Exception as e:
206+
logger.warning(f"重新获取 BDMV 文件详细信息失败: {e}")
207+
208+
# 处理文件
209+
await self.__file_processer(largest_file)
210+
211+
# 添加到已处理路径列表
212+
local_path = self.__get_local_path(largest_file)
213+
self.processed_local_paths.add(local_path)
214+
215+
logger.info(f"BDMV 文件处理完成: {largest_file.name}")
216+
except Exception as e:
217+
logger.error(f"处理 BDMV 文件 {largest_file.full_path} 时出错:{e}")
218+
logger.error(f"详细错误信息: {traceback.format_exc()}")
219+
continue
220+
164221
if self.sync_server:
165222
await self.__cleanup_local_files()
166223
logger.info("清理过期的 .strm 文件完成")
@@ -173,7 +230,9 @@ async def __file_processer(self, path: AlistPath) -> None:
173230
:param path: AlistPath 对象
174231
"""
175232
local_path = self.__get_local_path(path)
233+
logger.debug(f"__file_processer: 处理文件 {path.full_path} -> 本地路径 {local_path} | 模式 {self.mode}")
176234

235+
# 统一的 URL 生成逻辑,BDMV 文件与普通文件使用相同的逻辑
177236
if self.mode == "AlistURL":
178237
content = path.download_url
179238
elif self.mode == "RawURL":
@@ -183,9 +242,21 @@ async def __file_processer(self, path: AlistPath) -> None:
183242
else:
184243
raise ValueError(f"AlistStrm 未知的模式 {self.mode}")
185244

245+
logger.debug(f"__file_processer: 初始 content = {content}")
246+
247+
# 如果 URL 为空,提供 fallback(适用于所有文件类型)
248+
if not content:
249+
if self.mode == "AlistURL":
250+
content = f"{self.client.url}/d{path.full_path}"
251+
elif self.mode == "RawURL":
252+
content = path.download_url or f"{self.client.url}/d{path.full_path}"
253+
elif self.mode == "AlistPath":
254+
content = path.full_path
255+
logger.debug(f"__file_processer: fallback content = {content}")
256+
186257
await to_thread(local_path.parent.mkdir, parents=True, exist_ok=True)
187258

188-
logger.debug(f"开始处理 {local_path}")
259+
logger.debug(f"开始处理 {local_path} | 内容: {content}")
189260
if local_path.suffix == ".strm":
190261
async with async_open(local_path, mode="w", encoding="utf-8") as file:
191262
await file.write(content)
@@ -202,6 +273,27 @@ def __get_local_path(self, path: AlistPath) -> Path:
202273
:param path: AlistPath 对象
203274
:return: 本地文件路径
204275
"""
276+
# 检查是否为 BDMV 文件
277+
if self._is_bdmv_file(path):
278+
bdmv_root = self._get_bdmv_root_dir(path)
279+
if bdmv_root and self._should_process_bdmv_file(path):
280+
# 为 BDMV 文件生成特殊路径
281+
movie_title = self._get_movie_title_from_bdmv_path(bdmv_root)
282+
283+
if self.flatten_mode:
284+
local_path = self.target_dir / f"{movie_title}.strm"
285+
else:
286+
# 计算相对于 source_dir 的路径
287+
relative_path = bdmv_root.replace(self.source_dir, "", 1)
288+
if relative_path.startswith("/"):
289+
relative_path = relative_path[1:]
290+
291+
# 将 .strm 文件放在电影根目录下,使用电影标题命名
292+
local_path = self.target_dir / relative_path / f"{movie_title}.strm"
293+
294+
return local_path
295+
296+
# 原有逻辑保持不变
205297
if self.flatten_mode:
206298
local_path = self.target_dir / path.name
207299
else:
@@ -253,3 +345,92 @@ async def __cleanup_local_files(self) -> None:
253345
parent_dir = parent_dir.parent
254346
except Exception as e:
255347
logger.error(f"删除文件 {file_path} 失败:{e}")
348+
349+
def _is_bdmv_file(self, path: AlistPath) -> bool:
350+
"""
351+
检查文件是否为 BDMV 结构中的 .m2ts 文件
352+
353+
:param path: AlistPath 对象
354+
:return: 是否为 BDMV 文件
355+
"""
356+
return "/BDMV/STREAM/" in path.full_path and path.suffix.lower() == ".m2ts"
357+
358+
def _get_bdmv_root_dir(self, path: AlistPath) -> str:
359+
"""
360+
获取 BDMV 文件的根目录路径
361+
362+
:param path: BDMV 中的文件路径
363+
:return: BDMV 根目录路径
364+
"""
365+
full_path = path.full_path
366+
bdmv_index = full_path.find("/BDMV/")
367+
if bdmv_index != -1:
368+
return full_path[:bdmv_index]
369+
return ""
370+
371+
def _get_movie_title_from_bdmv_path(self, bdmv_root: str) -> str:
372+
"""
373+
从 BDMV 根目录路径提取电影标题
374+
375+
:param bdmv_root: BDMV 根目录路径
376+
:return: 电影标题
377+
"""
378+
# 获取最后一个目录名作为电影标题
379+
return Path(bdmv_root).name
380+
381+
def _collect_bdmv_file(self, path: AlistPath) -> None:
382+
"""
383+
收集 BDMV 文件信息
384+
385+
:param path: BDMV 中的 .m2ts 文件路径
386+
"""
387+
bdmv_root = self._get_bdmv_root_dir(path)
388+
if not bdmv_root:
389+
return
390+
391+
if bdmv_root not in self.bdmv_collections:
392+
self.bdmv_collections[bdmv_root] = []
393+
394+
# 添加文件信息到集合中
395+
self.bdmv_collections[bdmv_root].append((path, path.size))
396+
logger.debug(f"收集 BDMV 文件: {path.full_path}, 大小: {path.size}")
397+
398+
def _finalize_bdmv_collections(self) -> None:
399+
"""
400+
完成 BDMV 文件收集,确定每个 BDMV 目录中的最大文件
401+
"""
402+
for bdmv_root, files in self.bdmv_collections.items():
403+
if not files:
404+
continue
405+
406+
movie_title = self._get_movie_title_from_bdmv_path(bdmv_root)
407+
logger.info(f"BDMV 目录 '{movie_title}' 中发现 {len(files)} 个 .m2ts 文件:")
408+
409+
# 按大小排序并显示所有文件
410+
sorted_files = sorted(files, key=lambda x: x[1], reverse=True)
411+
for i, (file_path, file_size) in enumerate(sorted_files):
412+
size_mb = file_size / (1024 * 1024)
413+
status = "✓ 选中" if i == 0 else " 跳过"
414+
logger.info(f" {status} {file_path.name}: {size_mb:.1f} MB ({file_size} 字节)")
415+
416+
# 找出最大的文件
417+
largest_file = max(files, key=lambda x: x[1])
418+
self.bdmv_largest_files[bdmv_root] = largest_file[0]
419+
420+
largest_size_mb = largest_file[1] / (1024 * 1024)
421+
logger.info(f"BDMV 目录 '{movie_title}' 最终选择: {largest_file[0].name} ({largest_size_mb:.1f} MB)")
422+
423+
def _should_process_bdmv_file(self, path: AlistPath) -> bool:
424+
"""
425+
检查 BDMV 文件是否应该被处理(即是否为最大文件)
426+
427+
:param path: BDMV 中的 .m2ts 文件路径
428+
:return: 是否应该处理
429+
"""
430+
bdmv_root = self._get_bdmv_root_dir(path)
431+
if not bdmv_root:
432+
return False
433+
434+
largest_file = self.bdmv_largest_files.get(bdmv_root)
435+
return largest_file is not None and largest_file.full_path == path.full_path
436+

0 commit comments

Comments
 (0)