mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-06 07:11:26 +08:00
Merge branch 'jxxghp:v2' into v2
This commit is contained in:
@@ -315,21 +315,6 @@ class MediaChain(ChainBase):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_bluray_folder(fileitem: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
判断是否为原盘目录
|
|
||||||
"""
|
|
||||||
if not fileitem or fileitem.type != "dir":
|
|
||||||
return False
|
|
||||||
# 蓝光原盘目录必备的文件或文件夹
|
|
||||||
required_files = ['BDMV', 'CERTIFICATE']
|
|
||||||
# 检查目录下是否存在所需文件或文件夹
|
|
||||||
for item in StorageChain().list_files(fileitem):
|
|
||||||
if item.name in required_files:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@eventmanager.register(EventType.MetadataScrape)
|
@eventmanager.register(EventType.MetadataScrape)
|
||||||
def scrape_metadata_event(self, event: Event):
|
def scrape_metadata_event(self, event: Event):
|
||||||
"""
|
"""
|
||||||
@@ -370,7 +355,7 @@ class MediaChain(ChainBase):
|
|||||||
else:
|
else:
|
||||||
if file_list:
|
if file_list:
|
||||||
# 如果是BDMV原盘目录,只对根目录进行刮削,不处理子目录
|
# 如果是BDMV原盘目录,只对根目录进行刮削,不处理子目录
|
||||||
if self.is_bluray_folder(fileitem):
|
if storagechain.is_bluray_folder(fileitem):
|
||||||
logger.info(f"检测到BDMV原盘目录,只对根目录进行刮削:{fileitem.path}")
|
logger.info(f"检测到BDMV原盘目录,只对根目录进行刮削:{fileitem.path}")
|
||||||
self.scrape_metadata(fileitem=fileitem,
|
self.scrape_metadata(fileitem=fileitem,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
@@ -563,10 +548,23 @@ class MediaChain(ChainBase):
|
|||||||
logger.info("电影NFO刮削已关闭,跳过")
|
logger.info("电影NFO刮削已关闭,跳过")
|
||||||
else:
|
else:
|
||||||
# 电影目录
|
# 电影目录
|
||||||
if recursive:
|
files = __list_files(_fileitem=fileitem)
|
||||||
# 处理文件
|
is_bluray_folder = storagechain.contains_bluray_subdirectories(files)
|
||||||
if self.is_bluray_folder(fileitem):
|
if recursive and not is_bluray_folder:
|
||||||
# 原盘目录
|
# 处理非原盘目录内的文件
|
||||||
|
for file in files:
|
||||||
|
if file.type == "dir":
|
||||||
|
# 电影不处理子目录
|
||||||
|
continue
|
||||||
|
self.scrape_metadata(fileitem=file,
|
||||||
|
mediainfo=mediainfo,
|
||||||
|
init_folder=False,
|
||||||
|
parent=fileitem,
|
||||||
|
overwrite=overwrite)
|
||||||
|
# 生成目录内图片文件
|
||||||
|
if init_folder:
|
||||||
|
if is_bluray_folder:
|
||||||
|
# 检查电影NFO开关
|
||||||
if scraping_switchs.get('movie_nfo', True):
|
if scraping_switchs.get('movie_nfo', True):
|
||||||
nfo_path = filepath / (filepath.name + ".nfo")
|
nfo_path = filepath / (filepath.name + ".nfo")
|
||||||
if overwrite or not storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
if overwrite or not storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||||
@@ -581,20 +579,6 @@ class MediaChain(ChainBase):
|
|||||||
logger.info(f"已存在nfo文件:{nfo_path}")
|
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||||
else:
|
else:
|
||||||
logger.info("电影NFO刮削已关闭,跳过")
|
logger.info("电影NFO刮削已关闭,跳过")
|
||||||
else:
|
|
||||||
# 处理目录内的文件
|
|
||||||
files = __list_files(_fileitem=fileitem)
|
|
||||||
for file in files:
|
|
||||||
if file.type == "dir":
|
|
||||||
# 电影不处理子目录
|
|
||||||
continue
|
|
||||||
self.scrape_metadata(fileitem=file,
|
|
||||||
mediainfo=mediainfo,
|
|
||||||
init_folder=False,
|
|
||||||
parent=fileitem,
|
|
||||||
overwrite=overwrite)
|
|
||||||
# 生成目录内图片文件
|
|
||||||
if init_folder:
|
|
||||||
# 图片
|
# 图片
|
||||||
image_dict = self.metadata_img(mediainfo=mediainfo)
|
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||||
if image_dict:
|
if image_dict:
|
||||||
|
|||||||
@@ -133,22 +133,29 @@ class StorageChain(ChainBase):
|
|||||||
"""
|
"""
|
||||||
return self.run_module("support_transtype", storage=storage)
|
return self.run_module("support_transtype", storage=storage)
|
||||||
|
|
||||||
|
def is_bluray_folder(self, fileitem: Optional[schemas.FileItem]) -> bool:
|
||||||
|
"""
|
||||||
|
检查是否蓝光目录
|
||||||
|
"""
|
||||||
|
if not fileitem or fileitem.type != "dir":
|
||||||
|
return False
|
||||||
|
return self.contains_bluray_subdirectories(self.list_files(fileitem))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def contains_bluray_subdirectories(fileitems: Optional[List[schemas.FileItem]]) -> bool:
|
||||||
|
"""
|
||||||
|
判断是否包含蓝光必备的文件夹
|
||||||
|
"""
|
||||||
|
required_files = ("BDMV", "CERTIFICATE")
|
||||||
|
for item in fileitems or []:
|
||||||
|
if item.type == "dir" and item.name in required_files:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def delete_media_file(self, fileitem: schemas.FileItem, delete_self: bool = True) -> bool:
|
def delete_media_file(self, fileitem: schemas.FileItem, delete_self: bool = True) -> bool:
|
||||||
"""
|
"""
|
||||||
删除媒体文件,以及不含媒体文件的目录
|
删除媒体文件,以及不含媒体文件的目录
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __is_bluray_dir(_fileitem: schemas.FileItem) -> bool:
|
|
||||||
"""
|
|
||||||
检查是否蓝光目录
|
|
||||||
"""
|
|
||||||
_dir_files = self.list_files(fileitem=_fileitem, recursion=False)
|
|
||||||
if _dir_files:
|
|
||||||
for _f in _dir_files:
|
|
||||||
if _f.type == "dir" and _f.name in ["BDMV", "CERTIFICATE"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
media_exts = settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT
|
media_exts = settings.RMT_MEDIAEXT + settings.DOWNLOAD_TMPEXT
|
||||||
fileitem_path = Path(fileitem.path) if fileitem.path else Path("")
|
fileitem_path = Path(fileitem.path) if fileitem.path else Path("")
|
||||||
if len(fileitem_path.parts) <= 2:
|
if len(fileitem_path.parts) <= 2:
|
||||||
@@ -156,7 +163,7 @@ class StorageChain(ChainBase):
|
|||||||
return False
|
return False
|
||||||
if fileitem.type == "dir":
|
if fileitem.type == "dir":
|
||||||
# 本身是目录
|
# 本身是目录
|
||||||
if __is_bluray_dir(fileitem):
|
if self.is_bluray_folder(fileitem):
|
||||||
logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}】{fileitem.path}")
|
logger.warn(f"正在删除蓝光原盘目录:【{fileitem.storage}】{fileitem.path}")
|
||||||
if not self.delete_file(fileitem):
|
if not self.delete_file(fileitem):
|
||||||
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
logger.warn(f"【{fileitem.storage}】{fileitem.path} 删除失败")
|
||||||
|
|||||||
@@ -1635,7 +1635,7 @@ class SubscribeChain(ChainBase):
|
|||||||
info = schemas.SubscribeEpisodeInfo()
|
info = schemas.SubscribeEpisodeInfo()
|
||||||
info.title = episode.name
|
info.title = episode.name
|
||||||
info.description = episode.overview
|
info.description = episode.overview
|
||||||
info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}"
|
info.backdrop = settings.TMDB_IMAGE_URL(episode.still_path, "w500")
|
||||||
episodes[episode.episode_number] = info
|
episodes[episode.episode_number] = info
|
||||||
elif subscribe.type == MediaType.TV.value:
|
elif subscribe.type == MediaType.TV.value:
|
||||||
# 根据开始结束集计算集信息
|
# 根据开始结束集计算集信息
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
|||||||
self._transfer_interval = 15
|
self._transfer_interval = 15
|
||||||
# 事件管理器
|
# 事件管理器
|
||||||
self.jobview = JobManager()
|
self.jobview = JobManager()
|
||||||
# 车移成功的文件清单
|
# 转移成功的文件清单
|
||||||
self._success_target_files: Dict[str, List[str]] = {}
|
self._success_target_files: Dict[str, List[str]] = {}
|
||||||
# 启动整理任务
|
# 启动整理任务
|
||||||
self.__init()
|
self.__init()
|
||||||
@@ -873,7 +873,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
|||||||
state, errmsg = self.do_transfer(
|
state, errmsg = self.do_transfer(
|
||||||
fileitem=FileItem(
|
fileitem=FileItem(
|
||||||
storage="local",
|
storage="local",
|
||||||
path=file_path.as_posix(),
|
path=file_path.as_posix() + ("/" if file_path.is_dir() else ""),
|
||||||
type="dir" if not file_path.is_file() else "file",
|
type="dir" if not file_path.is_file() else "file",
|
||||||
name=file_path.name,
|
name=file_path.name,
|
||||||
size=file_path.stat().st_size,
|
size=file_path.stat().st_size,
|
||||||
@@ -908,16 +908,6 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
storagechain = StorageChain()
|
storagechain = StorageChain()
|
||||||
|
|
||||||
def __contains_bluray_sub(_fileitems: List[FileItem]) -> bool:
|
|
||||||
"""
|
|
||||||
判断是否包含蓝光子目录
|
|
||||||
"""
|
|
||||||
if _fileitems:
|
|
||||||
for sub in _fileitems:
|
|
||||||
if sub.type == "dir" and sub.name in ["BDMV", "CERTIFICATE"]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __is_bluray_sub(_path: str) -> bool:
|
def __is_bluray_sub(_path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否蓝光原盘目录内的子目录或文件
|
判断是否蓝光原盘目录内的子目录或文件
|
||||||
@@ -949,7 +939,7 @@ class TransferChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
# 蓝光原盘根目录
|
# 蓝光原盘根目录
|
||||||
sub_items = storagechain.list_files(fileitem) or []
|
sub_items = storagechain.list_files(fileitem) or []
|
||||||
if __contains_bluray_sub(sub_items):
|
if storagechain.contains_bluray_subdirectories(sub_items):
|
||||||
return [(fileitem, True)]
|
return [(fileitem, True)]
|
||||||
|
|
||||||
# 需要整理的文件项列表
|
# 需要整理的文件项列表
|
||||||
|
|||||||
@@ -843,6 +843,18 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
|
|||||||
rename_format = re.sub(r'/+', '/', rename_format)
|
rename_format = re.sub(r'/+', '/', rename_format)
|
||||||
return rename_format.strip("/")
|
return rename_format.strip("/")
|
||||||
|
|
||||||
|
def TMDB_IMAGE_URL(self, file_path: str, file_size: str = "original") -> str:
|
||||||
|
"""
|
||||||
|
获取TMDB图片网址
|
||||||
|
|
||||||
|
:param file_path: TMDB API返回的xxx_path
|
||||||
|
:param file_size: 图片大小,例如:'original', 'w500' 等
|
||||||
|
:return: 图片的完整URL
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f"https://{self.TMDB_IMAGE_DOMAIN}/t/p/{file_size}/{file_path.removeprefix('/')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 实例化配置
|
# 实例化配置
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
@@ -479,11 +479,11 @@ class MediaInfo:
|
|||||||
self.episode_groups = info.pop("episode_groups").get("results") or []
|
self.episode_groups = info.pop("episode_groups").get("results") or []
|
||||||
|
|
||||||
# 海报
|
# 海报
|
||||||
if info.get('poster_path'):
|
if path := info.get('poster_path'):
|
||||||
self.poster_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('poster_path')}"
|
self.poster_path = settings.TMDB_IMAGE_URL(path)
|
||||||
# 背景
|
# 背景
|
||||||
if info.get('backdrop_path'):
|
if path := info.get('backdrop_path'):
|
||||||
self.backdrop_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{info.get('backdrop_path')}"
|
self.backdrop_path = settings.TMDB_IMAGE_URL(path)
|
||||||
# 导演和演员
|
# 导演和演员
|
||||||
self.directors, self.actors = __directors_actors(info)
|
self.directors, self.actors = __directors_actors(info)
|
||||||
# 别名和译名
|
# 别名和译名
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ class TransHandler:
|
|||||||
return self.result.model_copy()
|
return self.result.model_copy()
|
||||||
else:
|
else:
|
||||||
new_path = target_path / fileitem.name
|
new_path = target_path / fileitem.name
|
||||||
|
# 在整理目录前先尝试获取原盘大小,避免整理记录出现0字节的情况
|
||||||
|
# TODO 当前只计算STREAM目录内的文件大小,如果需要精确则递归完整目录
|
||||||
|
if stream_fileitem := source_oper.get_item(
|
||||||
|
Path(fileitem.path) / "BDMV" / "STREAM"
|
||||||
|
):
|
||||||
|
fileitem.size = 0
|
||||||
|
files = source_oper.list(stream_fileitem) or []
|
||||||
|
for file in files:
|
||||||
|
fileitem.size += file.size
|
||||||
# 整理目录
|
# 整理目录
|
||||||
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
|
new_diritem, errmsg = self.__transfer_dir(fileitem=fileitem,
|
||||||
mediainfo=mediainfo,
|
mediainfo=mediainfo,
|
||||||
|
|||||||
@@ -867,19 +867,19 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
backdrops = images.get("backdrops")
|
backdrops = images.get("backdrops")
|
||||||
if backdrops:
|
if backdrops:
|
||||||
backdrops = sorted(backdrops, key=lambda x: x.get("vote_average"), reverse=True)
|
backdrops = sorted(backdrops, key=lambda x: x.get("vote_average"), reverse=True)
|
||||||
mediainfo.backdrop_path = backdrops[0].get("file_path")
|
mediainfo.backdrop_path = settings.TMDB_IMAGE_URL(backdrops[0].get("file_path"))
|
||||||
# 标志
|
# 标志
|
||||||
if not mediainfo.logo_path:
|
if not mediainfo.logo_path:
|
||||||
logos = images.get("logos")
|
logos = images.get("logos")
|
||||||
if logos:
|
if logos:
|
||||||
logos = sorted(logos, key=lambda x: x.get("vote_average"), reverse=True)
|
logos = sorted(logos, key=lambda x: x.get("vote_average"), reverse=True)
|
||||||
mediainfo.logo_path = logos[0].get("file_path")
|
mediainfo.logo_path = settings.TMDB_IMAGE_URL(logos[0].get("file_path"))
|
||||||
# 海报
|
# 海报
|
||||||
if not mediainfo.poster_path:
|
if not mediainfo.poster_path:
|
||||||
posters = images.get("posters")
|
posters = images.get("posters")
|
||||||
if posters:
|
if posters:
|
||||||
posters = sorted(posters, key=lambda x: x.get("vote_average"), reverse=True)
|
posters = sorted(posters, key=lambda x: x.get("vote_average"), reverse=True)
|
||||||
mediainfo.poster_path = posters[0].get("file_path")
|
mediainfo.poster_path = settings.TMDB_IMAGE_URL(posters[0].get("file_path"))
|
||||||
return mediainfo
|
return mediainfo
|
||||||
|
|
||||||
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
||||||
@@ -957,7 +957,7 @@ class TheMovieDbModule(_ModuleBase):
|
|||||||
image_path = seasoninfo.get(image_type.value)
|
image_path = seasoninfo.get(image_type.value)
|
||||||
|
|
||||||
if image_path:
|
if image_path:
|
||||||
return f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/{image_prefix}{image_path}"
|
return settings.TMDB_IMAGE_URL(image_path, image_prefix)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
|
def tmdb_movie_similar(self, tmdbid: int) -> List[MediaInfo]:
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ class TmdbScraper:
|
|||||||
seasoninfo = self.original_tmdb(mediainfo).get_tv_season_detail(mediainfo.tmdb_id, season)
|
seasoninfo = self.original_tmdb(mediainfo).get_tv_season_detail(mediainfo.tmdb_id, season)
|
||||||
if seasoninfo:
|
if seasoninfo:
|
||||||
episodeinfo = self.__get_episode_detail(seasoninfo, episode)
|
episodeinfo = self.__get_episode_detail(seasoninfo, episode)
|
||||||
if episodeinfo and episodeinfo.get("still_path"):
|
if still_path := episodeinfo.get("still_path"):
|
||||||
# TMDB集still图片
|
# TMDB集still图片
|
||||||
still_name = f"{episode}"
|
still_name = f"{episode}"
|
||||||
still_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episodeinfo.get('still_path')}"
|
still_url = settings.TMDB_IMAGE_URL(still_path)
|
||||||
images[still_name] = still_url
|
images[still_name] = still_url
|
||||||
else:
|
else:
|
||||||
# 季的图片
|
# 季的图片
|
||||||
@@ -115,7 +115,7 @@ class TmdbScraper:
|
|||||||
if _mediainfo:
|
if _mediainfo:
|
||||||
for attr_name, attr_value in _mediainfo.items():
|
for attr_name, attr_value in _mediainfo.items():
|
||||||
if attr_name.endswith("_path") and attr_value is not None:
|
if attr_name.endswith("_path") and attr_value is not None:
|
||||||
image_url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{attr_value}"
|
image_url = settings.TMDB_IMAGE_URL(attr_value)
|
||||||
image_name = attr_name.replace("_path", "") + Path(image_url).suffix
|
image_name = attr_name.replace("_path", "") + Path(image_url).suffix
|
||||||
images[image_name] = image_url
|
images[image_name] = image_url
|
||||||
return images
|
return images
|
||||||
@@ -127,11 +127,11 @@ class TmdbScraper:
|
|||||||
"""
|
"""
|
||||||
# TMDB季poster图片
|
# TMDB季poster图片
|
||||||
sea_seq = str(season).rjust(2, '0')
|
sea_seq = str(season).rjust(2, '0')
|
||||||
if seasoninfo.get("poster_path"):
|
if poster_path := seasoninfo.get("poster_path"):
|
||||||
# 后缀
|
# 后缀
|
||||||
ext = Path(seasoninfo.get('poster_path')).suffix
|
ext = Path(poster_path).suffix
|
||||||
# URL
|
# URL
|
||||||
url = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{seasoninfo.get('poster_path')}"
|
url = settings.TMDB_IMAGE_URL(poster_path)
|
||||||
# S0海报格式不同
|
# S0海报格式不同
|
||||||
if season == 0:
|
if season == 0:
|
||||||
image_name = f"season-specials-poster{ext}"
|
image_name = f"season-specials-poster{ext}"
|
||||||
@@ -190,8 +190,8 @@ class TmdbScraper:
|
|||||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||||
DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "")
|
DomUtils.add_node(doc, xactor, "role", actor.get("character") or actor.get("role") or "")
|
||||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||||
DomUtils.add_node(doc, xactor, "thumb",
|
if profile_path := actor.get('profile_path'):
|
||||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}")
|
DomUtils.add_node(doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path))
|
||||||
DomUtils.add_node(doc, xactor, "profile",
|
DomUtils.add_node(doc, xactor, "profile",
|
||||||
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
||||||
# 风格
|
# 风格
|
||||||
@@ -330,8 +330,8 @@ class TmdbScraper:
|
|||||||
DomUtils.add_node(doc, xactor, "name", actor.get("name") or "")
|
DomUtils.add_node(doc, xactor, "name", actor.get("name") or "")
|
||||||
DomUtils.add_node(doc, xactor, "type", "Actor")
|
DomUtils.add_node(doc, xactor, "type", "Actor")
|
||||||
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
DomUtils.add_node(doc, xactor, "tmdbid", actor.get("id") or "")
|
||||||
DomUtils.add_node(doc, xactor, "thumb",
|
if profile_path := actor.get('profile_path'):
|
||||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{actor.get('profile_path')}")
|
DomUtils.add_node(doc, xactor, "thumb", settings.TMDB_IMAGE_URL(profile_path))
|
||||||
DomUtils.add_node(doc, xactor, "profile",
|
DomUtils.add_node(doc, xactor, "profile",
|
||||||
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
f"https://www.themoviedb.org/person/{actor.get('id')}")
|
||||||
return doc
|
return doc
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class TmdbCache(metaclass=WeakSingleton):
|
|||||||
"""
|
"""
|
||||||
获取缓存KEY
|
获取缓存KEY
|
||||||
"""
|
"""
|
||||||
return f"[{meta.type.value if meta.type else '未知'}]{meta.tmdbid or meta.name}-{meta.year}-{meta.begin_season}"
|
return f"[{meta.type.value if meta.type else '未知'}][{settings.TMDB_LOCALE}]{meta.tmdbid or meta.name}-{meta.year}-{meta.begin_season}"
|
||||||
|
|
||||||
def get(self, meta: MetaBase):
|
def get(self, meta: MetaBase):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -826,7 +826,7 @@ class TmdbApi:
|
|||||||
# 转换多语种标题
|
# 转换多语种标题
|
||||||
self.__update_tmdbinfo_extra_title(tmdb_info)
|
self.__update_tmdbinfo_extra_title(tmdb_info)
|
||||||
# 转换中文标题
|
# 转换中文标题
|
||||||
if settings.TMDB_LOCALE == "zh":
|
if self.tmdb.language in ("zh", "zh-CN"):
|
||||||
self.__update_tmdbinfo_cn_title(tmdb_info)
|
self.__update_tmdbinfo_cn_title(tmdb_info)
|
||||||
|
|
||||||
return tmdb_info
|
return tmdb_info
|
||||||
@@ -2134,7 +2134,7 @@ class TmdbApi:
|
|||||||
# 转换多语种标题
|
# 转换多语种标题
|
||||||
self.__update_tmdbinfo_extra_title(tmdb_info)
|
self.__update_tmdbinfo_extra_title(tmdb_info)
|
||||||
# 转换中文标题
|
# 转换中文标题
|
||||||
if settings.TMDB_LOCALE == "zh":
|
if self.tmdb.language in ("zh", "zh-CN"):
|
||||||
self.__update_tmdbinfo_cn_title(tmdb_info)
|
self.__update_tmdbinfo_cn_title(tmdb_info)
|
||||||
|
|
||||||
return tmdb_info
|
return tmdb_info
|
||||||
|
|||||||
@@ -695,11 +695,13 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass):
|
|||||||
|
|
||||||
# 全程加锁
|
# 全程加锁
|
||||||
with lock:
|
with lock:
|
||||||
|
is_bluray_folder = False
|
||||||
# 蓝光原盘文件处理
|
# 蓝光原盘文件处理
|
||||||
if __is_bluray_sub(event_path):
|
if __is_bluray_sub(event_path):
|
||||||
event_path = __get_bluray_dir(event_path)
|
event_path = __get_bluray_dir(event_path)
|
||||||
if not event_path:
|
if not event_path:
|
||||||
return
|
return
|
||||||
|
is_bluray_folder = True
|
||||||
|
|
||||||
# TTL缓存控重
|
# TTL缓存控重
|
||||||
if self._cache.get(str(event_path)):
|
if self._cache.get(str(event_path)):
|
||||||
@@ -708,13 +710,20 @@ class Monitor(ConfigReloadMixin, metaclass=SingletonClass):
|
|||||||
self._cache[str(event_path)] = True
|
self._cache[str(event_path)] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"开始整理文件: {event_path}")
|
if is_bluray_folder:
|
||||||
|
logger.info(f"开始整理蓝光原盘: {event_path}")
|
||||||
|
else:
|
||||||
|
logger.info(f"开始整理文件: {event_path}")
|
||||||
# 开始整理
|
# 开始整理
|
||||||
TransferChain().do_transfer(
|
TransferChain().do_transfer(
|
||||||
fileitem=FileItem(
|
fileitem=FileItem(
|
||||||
storage=storage,
|
storage=storage,
|
||||||
path=event_path.as_posix(),
|
path=(
|
||||||
type="file",
|
event_path.as_posix()
|
||||||
|
if not is_bluray_folder
|
||||||
|
else event_path.as_posix() + "/"
|
||||||
|
),
|
||||||
|
type="file" if not is_bluray_folder else "dir",
|
||||||
name=event_path.name,
|
name=event_path.name,
|
||||||
basename=event_path.stem,
|
basename=event_path.stem,
|
||||||
extension=event_path.suffix[1:],
|
extension=event_path.suffix[1:],
|
||||||
|
|||||||
@@ -479,6 +479,8 @@ class SystemUtils:
|
|||||||
def is_bluray_dir(dir_path: Path) -> bool:
|
def is_bluray_dir(dir_path: Path) -> bool:
|
||||||
"""
|
"""
|
||||||
判断是否为蓝光原盘目录
|
判断是否为蓝光原盘目录
|
||||||
|
|
||||||
|
(该方法已弃用,改用`StorageChain().is_bluray_folder)`
|
||||||
"""
|
"""
|
||||||
if not dir_path.is_dir():
|
if not dir_path.is_dir():
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,161 +1,99 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
|
# 文件列表结构 list[tuple(名称, 子文件列表 或 文件大小)]
|
||||||
bluray_files = [
|
bluray_files = [
|
||||||
{
|
(
|
||||||
"name": "FOLDER",
|
"FOLDER",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "Digimon",
|
"Digimon",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "Digimon (2055)",
|
"Digimon BluRay (2055)",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "BDMV",
|
"BDMV",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "STREAM",
|
"STREAM",
|
||||||
"children": [
|
[
|
||||||
{
|
("00000.m2ts", 104857600),
|
||||||
"name": "00000.m2ts",
|
("00001.m2ts", 104857600),
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "00001.m2ts",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("CERTIFICATE", None),
|
||||||
"name": "CERTIFICATE",
|
|
||||||
"children": [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
(
|
||||||
"name": "Digimon (2099)",
|
"Digimon BluRay (2099)",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "BDMV",
|
"BDMV",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "STREAM",
|
"STREAM",
|
||||||
"children": [
|
[
|
||||||
{
|
("00000.m2ts", 104857600),
|
||||||
"name": "00000.m2ts",
|
("00001.m2ts", 104857600),
|
||||||
"size": 104857600,
|
("00002.m2ts.!qB", 104857600),
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "00001.m2ts",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "00002.m2ts.!qB",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("CERTIFICATE", None),
|
||||||
"name": "CERTIFICATE",
|
|
||||||
"children": [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("Digimon (2199)", [("Digimon.2199.mp4", 104857600)]),
|
||||||
"name": "Digimon (2199)",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"name": "Digimon.2199.mp4",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
(
|
||||||
"name": "Pokemon (2016)",
|
"Pokemon BluRay (2016)",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "BDMV",
|
"BDMV",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "STREAM",
|
"STREAM",
|
||||||
"children": [
|
[
|
||||||
{
|
("00000.m2ts", 104857600),
|
||||||
"name": "00000.m2ts",
|
("00001.m2ts", 104857600),
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "00001.m2ts",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
)
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("CERTIFICATE", None),
|
||||||
"name": "CERTIFICATE",
|
|
||||||
"children": [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
(
|
||||||
"name": "Pokemon (2021)",
|
"Pokemon BluRay (2021)",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "BDMV",
|
"BDMV",
|
||||||
"children": [
|
[
|
||||||
{
|
(
|
||||||
"name": "STREAM",
|
"STREAM",
|
||||||
"children": [
|
[
|
||||||
{
|
("00000.m2ts", 104857600),
|
||||||
"name": "00000.m2ts",
|
("00001.m2ts", 104857600),
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "00001.m2ts",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
)
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("CERTIFICATE", None),
|
||||||
"name": "CERTIFICATE",
|
|
||||||
"children": [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
(
|
||||||
"name": "Pokemon (2028)",
|
"Pokemon (2028)",
|
||||||
"children": [
|
[
|
||||||
{
|
("Pokemon.2028.mkv", 104857600),
|
||||||
"name": "Pokemon.2028.mkv",
|
("Pokemon.2028.hdr.mkv.!qB", 104857600),
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Pokemon.2028.hdr.mkv.!qB",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
),
|
||||||
{
|
("Pokemon.2029.mp4", 104857600),
|
||||||
"name": "Pokemon.2029.mp4",
|
("Pokemon.2039.mp4", 104857600),
|
||||||
"size": 104857600,
|
("Pokemon (2030)", [("S", 104857600)]),
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Pokemon (2030)",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"name": "S",
|
|
||||||
"size": 104857600,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from tests.test_bluray import BluRayTest
|
||||||
from tests.test_metainfo import MetaInfoTest
|
from tests.test_metainfo import MetaInfoTest
|
||||||
from tests.test_object import ObjectUtilsTest
|
from tests.test_object import ObjectUtilsTest
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ if __name__ == '__main__':
|
|||||||
suite.addTest(MetaInfoTest('test_emby_format_ids'))
|
suite.addTest(MetaInfoTest('test_emby_format_ids'))
|
||||||
suite.addTest(ObjectUtilsTest('test_check_method'))
|
suite.addTest(ObjectUtilsTest('test_check_method'))
|
||||||
|
|
||||||
|
# 测试蓝光目录识别
|
||||||
|
suite.addTest(BluRayTest())
|
||||||
|
|
||||||
# 运行测试
|
# 运行测试
|
||||||
runner = unittest.TextTestRunner()
|
runner = unittest.TextTestRunner()
|
||||||
runner.run(suite)
|
runner.run(suite)
|
||||||
|
|||||||
226
tests/test_bluray.py
Normal file
226
tests/test_bluray.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.chain.media import MediaChain
|
||||||
|
from app.chain.storage import StorageChain
|
||||||
|
from app.chain.transfer import TransferChain
|
||||||
|
from app.core.context import MediaInfo
|
||||||
|
from app.core.event import Event
|
||||||
|
from app.core.metainfo import MetaInfoPath
|
||||||
|
from app.db.models.transferhistory import TransferHistory
|
||||||
|
from app.log import logger
|
||||||
|
from app.schemas.types import EventType
|
||||||
|
from tests.cases.files import bluray_files
|
||||||
|
|
||||||
|
|
||||||
|
class BluRayTest(TestCase):
|
||||||
|
def __init__(self, methodName="test"):
|
||||||
|
super().__init__(methodName)
|
||||||
|
self.__history = []
|
||||||
|
self.__root = schemas.FileItem(
|
||||||
|
path="/", name="", type="dir", extension="", size=0
|
||||||
|
)
|
||||||
|
self.__all = {self.__root.path: self.__root}
|
||||||
|
|
||||||
|
def __build_child(parent: schemas.FileItem, files: list[tuple[str, list | int]]):
|
||||||
|
parent.children = []
|
||||||
|
for name, children in files:
|
||||||
|
sep = "" if parent.path.endswith("/") else "/"
|
||||||
|
file_item = schemas.FileItem(
|
||||||
|
path=f"{parent.path}{sep}{name}",
|
||||||
|
name=name,
|
||||||
|
extension=Path(name).suffix[1:],
|
||||||
|
basename=Path(name).stem,
|
||||||
|
type="file" if isinstance(children, int) else "dir",
|
||||||
|
size=children if isinstance(children, int) else 0,
|
||||||
|
)
|
||||||
|
parent.children.append(file_item)
|
||||||
|
self.__all[file_item.path] = file_item
|
||||||
|
if isinstance(children, list):
|
||||||
|
__build_child(file_item, children)
|
||||||
|
|
||||||
|
__build_child(self.__root, bluray_files)
|
||||||
|
|
||||||
|
def _test_do_transfer(self):
|
||||||
|
def __test_do_transfer(path: str):
|
||||||
|
self.__history.clear()
|
||||||
|
TransferChain().do_transfer(
|
||||||
|
force=False,
|
||||||
|
background=False,
|
||||||
|
fileitem=StorageChain().get_file_item(None, Path(path)),
|
||||||
|
)
|
||||||
|
return self.__history
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2099)",
|
||||||
|
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
],
|
||||||
|
__test_do_transfer(
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)/BDMV/STREAM/00001.m2ts"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon/Digimon (2199)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Pokemon.2029.mp4",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER/Pokemon.2029.mp4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2055)",
|
||||||
|
"/FOLDER/Digimon/Digimon BluRay (2099)",
|
||||||
|
"/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4",
|
||||||
|
"/FOLDER/Pokemon BluRay (2016)",
|
||||||
|
"/FOLDER/Pokemon BluRay (2021)",
|
||||||
|
"/FOLDER/Pokemon (2028)/Pokemon.2028.mkv",
|
||||||
|
"/FOLDER/Pokemon.2029.mp4",
|
||||||
|
"/FOLDER/Pokemon.2039.mp4",
|
||||||
|
],
|
||||||
|
__test_do_transfer("/FOLDER"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _test_scrape_metadata(self, mock_metadata_nfo):
|
||||||
|
def __test_scrape_metadata(path: str, excepted_nfo_count: int = 1):
|
||||||
|
"""
|
||||||
|
分别测试手动和自动刮削
|
||||||
|
"""
|
||||||
|
fileitem = StorageChain().get_file_item(None, Path(path))
|
||||||
|
meta = MetaInfoPath(Path(fileitem.path))
|
||||||
|
mediainfo = MediaInfo(tmdb_info={"id": 1, "title": "Test"})
|
||||||
|
|
||||||
|
# 测试手动刮削
|
||||||
|
logger.debug(f"测试手动刮削 {path}")
|
||||||
|
mock_metadata_nfo.call_count = 0
|
||||||
|
MediaChain().scrape_metadata(
|
||||||
|
fileitem=fileitem, meta=meta, mediainfo=mediainfo, overwrite=True
|
||||||
|
)
|
||||||
|
# 确保调用了指定次数的metadata_nfo
|
||||||
|
self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count)
|
||||||
|
|
||||||
|
# 测试自动刮削
|
||||||
|
logger.debug(f"测试自动刮削 {path}")
|
||||||
|
mock_metadata_nfo.call_count = 0
|
||||||
|
MediaChain().scrape_metadata_event(
|
||||||
|
Event(
|
||||||
|
event_type=EventType.MetadataScrape,
|
||||||
|
event_data={
|
||||||
|
"meta": meta,
|
||||||
|
"mediainfo": mediainfo,
|
||||||
|
"fileitem": fileitem,
|
||||||
|
"file_list": [fileitem.path],
|
||||||
|
"overwrite": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 调用了指定次数的metadata_nfo
|
||||||
|
self.assertEqual(mock_metadata_nfo.call_count, excepted_nfo_count)
|
||||||
|
|
||||||
|
# 刮削原盘目录
|
||||||
|
__test_scrape_metadata("/FOLDER/Digimon/Digimon BluRay (2099)")
|
||||||
|
# 刮削电影文件
|
||||||
|
__test_scrape_metadata("/FOLDER/Digimon/Digimon (2199)/Digimon.2199.mp4")
|
||||||
|
# 刮削电影目录
|
||||||
|
__test_scrape_metadata("/FOLDER", excepted_nfo_count=2)
|
||||||
|
|
||||||
|
@patch("app.chain.ChainBase.metadata_img", return_value=None) # 避免获取图片
|
||||||
|
@patch("app.chain.ChainBase.__init__", return_value=None) # 避免不必要的模块初始化
|
||||||
|
@patch("app.db.transferhistory_oper.TransferHistoryOper.get_by_src")
|
||||||
|
@patch("app.chain.storage.StorageChain.list_files")
|
||||||
|
@patch("app.chain.storage.StorageChain.get_parent_item")
|
||||||
|
@patch("app.chain.storage.StorageChain.get_file_item")
|
||||||
|
def test(
|
||||||
|
self,
|
||||||
|
mock_get_file_item,
|
||||||
|
mock_get_parent_item,
|
||||||
|
mock_list_files,
|
||||||
|
mock_get_by_src,
|
||||||
|
*_,
|
||||||
|
):
|
||||||
|
def get_file_item(storage: str, path: Path):
|
||||||
|
path_posix = path.as_posix()
|
||||||
|
return self.__all.get(path_posix)
|
||||||
|
|
||||||
|
def get_parent_item(fileitem: schemas.FileItem):
|
||||||
|
return get_file_item(None, Path(fileitem.path).parent)
|
||||||
|
|
||||||
|
def list_files(fileitem: schemas.FileItem, recursion: bool = False):
|
||||||
|
if fileitem.type != "dir":
|
||||||
|
return None
|
||||||
|
if recursion:
|
||||||
|
result = []
|
||||||
|
file_path = f"{fileitem.path}/"
|
||||||
|
for path, item in self.__all.items():
|
||||||
|
if path.startswith(file_path):
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return fileitem.children
|
||||||
|
|
||||||
|
def get_by_src(src: str, storage: Optional[str] = None):
|
||||||
|
self.__history.append(src)
|
||||||
|
result = TransferHistory()
|
||||||
|
result.status = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
mock_get_file_item.side_effect = get_file_item
|
||||||
|
mock_get_parent_item.side_effect = get_parent_item
|
||||||
|
mock_list_files.side_effect = list_files
|
||||||
|
mock_get_by_src.side_effect = get_by_src
|
||||||
|
|
||||||
|
self._test_do_transfer()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"app.chain.media.MediaChain.metadata_nfo", return_value=None
|
||||||
|
) as mock:
|
||||||
|
self._test_scrape_metadata(mock_metadata_nfo=mock)
|
||||||
Reference in New Issue
Block a user