mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
feat(music): 完善歌词与专辑目录整理
This commit is contained in:
@@ -141,6 +141,12 @@ class FileFilterMixin:
|
||||
return False
|
||||
return True if f".{fileitem.extension.lower()}" in self._audio_exts else False
|
||||
|
||||
@staticmethod
|
||||
def _is_music_lyrics_file(fileitem: FileItem) -> bool:
|
||||
"""判断文件是否为可随同名音乐音轨迁移的歌词旁挂文件。"""
|
||||
path = str(fileitem.path or fileitem.name or "").casefold()
|
||||
return path.endswith((".lrc", ".txt", ".lyricsfile.yaml"))
|
||||
|
||||
def _is_media_file(
|
||||
self,
|
||||
fileitem: FileItem,
|
||||
@@ -1068,6 +1074,11 @@ class FileKeyMixin:
|
||||
"""
|
||||
if self._is_subtitle_file(extra_fileitem):
|
||||
return self._get_subtitle_media_stem(extra_fileitem)
|
||||
if self._is_music_lyrics_file(extra_fileitem):
|
||||
file_name = extra_fileitem.name or Path(extra_fileitem.path).name
|
||||
lowered = file_name.casefold()
|
||||
suffix = ".lyricsfile.yaml" if lowered.endswith(".lyricsfile.yaml") else Path(file_name).suffix
|
||||
return file_name[:-len(suffix)].casefold() if suffix else lowered
|
||||
return self._get_file_stem(extra_fileitem)
|
||||
|
||||
def _get_related_main_file_key(
|
||||
@@ -1081,6 +1092,7 @@ class FileKeyMixin:
|
||||
if not (
|
||||
self._is_subtitle_file(extra_fileitem)
|
||||
or self._is_audio_file(extra_fileitem)
|
||||
or self._is_music_lyrics_file(extra_fileitem)
|
||||
):
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.domain.context import MusicInfo, MusicLyrics
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
|
||||
|
||||
class LrclibChain(ChainBase):
|
||||
"""LRCLIB 音乐歌词来源链。"""
|
||||
|
||||
def get_music_lyrics(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
) -> Optional[MusicLyrics]:
|
||||
"""按单曲元数据获取标准化歌词。"""
|
||||
result = self.run_module("music_lyrics", music=music)
|
||||
if isinstance(result, MusicLyrics):
|
||||
return result
|
||||
return MusicLyrics.from_dict(result) if isinstance(result, dict) else None
|
||||
|
||||
async def async_get_music_lyrics(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
) -> Optional[MusicLyrics]:
|
||||
"""异步按单曲元数据获取标准化歌词。"""
|
||||
result = await self.async_run_module("music_lyrics", music=music)
|
||||
if isinstance(result, MusicLyrics):
|
||||
return result
|
||||
return MusicLyrics.from_dict(result) if isinstance(result, dict) else None
|
||||
@@ -0,0 +1,128 @@
|
||||
import time
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from app.chain import ChainBase
|
||||
from app.domain.context import MusicInfo, MusicLyrics
|
||||
from app.domain.meta.metamusic import MetaMusic
|
||||
|
||||
|
||||
class LyricsChain(ChainBase):
|
||||
"""聚合本地、插件和宿主歌词候选,并按匹配度与内容质量择优。"""
|
||||
|
||||
def get_music_lyrics_candidates(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
local_candidates: Optional[list[MusicLyrics]] = None,
|
||||
) -> list[MusicLyrics]:
|
||||
"""获取所有标准化歌词候选,同时兼容旧版单结果插件接口。"""
|
||||
candidates = list(local_candidates or [])
|
||||
if self.deadline is not None and time.monotonic() >= self.deadline:
|
||||
self.budget_exceeded = True
|
||||
return self._deduplicate(candidates)
|
||||
legacy = self.run_module("music_lyrics", music=music)
|
||||
candidates.extend(self._normalize_results(legacy))
|
||||
results = self.run_module("music_lyrics_candidates", music=music)
|
||||
candidates.extend(self._normalize_results(results))
|
||||
|
||||
fallback = str(getattr(music, "lyrics", None) or "").strip()
|
||||
if fallback:
|
||||
candidates.append(MusicLyrics(
|
||||
provider="theaudiodb",
|
||||
plain_lyrics=fallback,
|
||||
match_score=85,
|
||||
provider_priority=10,
|
||||
))
|
||||
return self._deduplicate(candidates)
|
||||
|
||||
def get_music_lyrics(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
local_candidates: Optional[list[MusicLyrics]] = None,
|
||||
) -> Optional[MusicLyrics]:
|
||||
"""返回匹配可信且质量最高的歌词候选。"""
|
||||
candidates = self.get_music_lyrics_candidates(music, local_candidates)
|
||||
return self._select_best(candidates)
|
||||
|
||||
async def async_get_music_lyrics_candidates(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
local_candidates: Optional[list[MusicLyrics]] = None,
|
||||
) -> list[MusicLyrics]:
|
||||
"""异步聚合新旧模块候选,并复用同步路径的本地兜底规则。"""
|
||||
candidates = list(local_candidates or [])
|
||||
if self.deadline is not None and time.monotonic() >= self.deadline:
|
||||
self.budget_exceeded = True
|
||||
return self._deduplicate(candidates)
|
||||
legacy = await self.async_run_module("music_lyrics", music=music)
|
||||
candidates.extend(self._normalize_results(legacy))
|
||||
results = await self.async_run_module("music_lyrics_candidates", music=music)
|
||||
candidates.extend(self._normalize_results(results))
|
||||
fallback = str(getattr(music, "lyrics", None) or "").strip()
|
||||
if fallback:
|
||||
candidates.append(MusicLyrics(
|
||||
provider="theaudiodb",
|
||||
plain_lyrics=fallback,
|
||||
match_score=85,
|
||||
provider_priority=10,
|
||||
))
|
||||
return self._deduplicate(candidates)
|
||||
|
||||
async def async_get_music_lyrics(
|
||||
self,
|
||||
music: Union[MetaMusic, MusicInfo],
|
||||
local_candidates: Optional[list[MusicLyrics]] = None,
|
||||
) -> Optional[MusicLyrics]:
|
||||
"""异步返回质量最高的标准歌词候选。"""
|
||||
candidates = await self.async_get_music_lyrics_candidates(music, local_candidates)
|
||||
return self._select_best(candidates)
|
||||
|
||||
@staticmethod
|
||||
def _select_best(candidates: list[MusicLyrics]) -> Optional[MusicLyrics]:
|
||||
"""在可信候选中按内容质量、匹配度和来源优先级择优。"""
|
||||
if not candidates:
|
||||
return None
|
||||
return max(
|
||||
candidates,
|
||||
key=lambda item: (
|
||||
item.quality_rank,
|
||||
item.match_score,
|
||||
item.provider_priority,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _normalize_results(cls, value: Any) -> list[MusicLyrics]:
|
||||
"""把模块的单结果、列表或字典结果统一转换为候选列表。"""
|
||||
values = value if isinstance(value, list) else [value]
|
||||
normalized = []
|
||||
for item in values:
|
||||
if isinstance(item, MusicLyrics):
|
||||
normalized.append(item)
|
||||
elif isinstance(item, dict):
|
||||
normalized.append(MusicLyrics.from_dict(item))
|
||||
return normalized
|
||||
|
||||
@staticmethod
|
||||
def _deduplicate(candidates: list[MusicLyrics]) -> list[MusicLyrics]:
|
||||
"""按来源身份和内容去重,重复项保留质量与匹配度更高的一条。"""
|
||||
selected: dict[tuple[str, str, str], MusicLyrics] = {}
|
||||
for candidate in candidates:
|
||||
if not candidate or not (candidate.content or candidate.instrumental):
|
||||
continue
|
||||
current = selected.get(candidate.identity_key)
|
||||
if current is None or (
|
||||
candidate.match_score,
|
||||
candidate.quality_rank,
|
||||
candidate.provider_priority,
|
||||
) > (
|
||||
current.match_score,
|
||||
current.quality_rank,
|
||||
current.provider_priority,
|
||||
):
|
||||
selected[candidate.identity_key] = candidate
|
||||
return list(selected.values())
|
||||
def __init__(self, deadline: Optional[float] = None) -> None:
|
||||
"""保存批次查询截止时间,防止单张专辑长期占用刮削任务。"""
|
||||
super().__init__()
|
||||
self.deadline = deadline
|
||||
self.budget_exceeded = False
|
||||
+95
-13
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -14,7 +15,7 @@ from app.application.configuration import (
|
||||
get_configured_system_config,
|
||||
)
|
||||
from app.chain import ChainBase
|
||||
from app.chain.lrclib import LrclibChain
|
||||
from app.chain.lyrics import LyricsChain
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.storage import StorageChain
|
||||
from app.domain.context import (
|
||||
@@ -102,6 +103,11 @@ class ScrapingOption:
|
||||
"""是否覆盖模式"""
|
||||
return self.policy == ScrapingPolicy.OVERWRITE
|
||||
|
||||
@property
|
||||
def is_upgrade(self) -> bool:
|
||||
"""是否只在歌词等产物质量更高时替换。"""
|
||||
return self.policy == ScrapingPolicy.UPGRADE
|
||||
|
||||
class ScrapingConfig:
|
||||
"""媒体刮削配置"""
|
||||
|
||||
@@ -165,7 +171,9 @@ class ScrapingConfig:
|
||||
]
|
||||
for md in mds
|
||||
]
|
||||
return {item: ScrapingPolicy.MISSINGONLY for item in config_items}
|
||||
defaults = {item: ScrapingPolicy.MISSINGONLY for item in config_items}
|
||||
defaults["music_lyrics"] = ScrapingPolicy.UPGRADE
|
||||
return defaults
|
||||
|
||||
|
||||
class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
@@ -194,7 +202,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"landscape": ["thumb"],
|
||||
}
|
||||
|
||||
MUSIC_LYRICS_EXTENSIONS = (".lrc", ".txt")
|
||||
MUSIC_LYRICS_EXTENSIONS = (".lyricsfile.yaml", ".lrc", ".txt")
|
||||
_music_track_prefix_pattern = re.compile(
|
||||
r"^\s*(?:(?:cd|disc)\s*\d+\s*[-_. ]+)?(?:\d+\s*[-_. ]+)+",
|
||||
flags=re.IGNORECASE,
|
||||
@@ -1045,7 +1053,9 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
with_cover = not poster_option.is_skip
|
||||
lyrics_chain = None
|
||||
if not lyrics_option.is_skip:
|
||||
lyrics_chain = LrclibChain()
|
||||
lyrics_chain = LyricsChain(
|
||||
deadline=time.monotonic() + max(self.runtime_config.lyrics_batch_timeout, 0)
|
||||
)
|
||||
cover_cache: dict[str, tuple[Optional[bytes], str]] = {}
|
||||
album_cache: dict[tuple[str, str], Optional[MusicAlbumInfo]] = {}
|
||||
|
||||
@@ -1054,6 +1064,9 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"saved": 0,
|
||||
"existing": 0,
|
||||
"missing": 0,
|
||||
"upgraded": 0,
|
||||
"protected": 0,
|
||||
"budget_exceeded": 0,
|
||||
"failed": 0,
|
||||
}
|
||||
metadata_failure_label = (
|
||||
@@ -1109,11 +1122,15 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
if not lyrics_option.is_skip:
|
||||
message += (
|
||||
f",歌词新增 {lyrics_counts['saved']} 首"
|
||||
f"、升级 {lyrics_counts['upgraded']} 首"
|
||||
f"、已存在 {lyrics_counts['existing']} 首"
|
||||
f"、防降级保护 {lyrics_counts['protected']} 首"
|
||||
f"、未匹配 {lyrics_counts['missing']} 首"
|
||||
)
|
||||
if lyrics_counts["failed"]:
|
||||
message += f"、失败 {lyrics_counts['failed']} 首"
|
||||
if lyrics_counts["budget_exceeded"]:
|
||||
message += f"、预算耗尽 {lyrics_counts['budget_exceeded']} 首"
|
||||
if failures:
|
||||
return False, f"{message};{';'.join(failures[:3])}"
|
||||
return True, message
|
||||
@@ -1240,7 +1257,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
cover: Optional[tuple[Optional[bytes], str]] = None,
|
||||
lyrics_option: Optional[ScrapingOption] = None,
|
||||
lyrics_overwrite: bool = False,
|
||||
lyrics_chain: Optional[LrclibChain] = None,
|
||||
lyrics_chain: Optional[LyricsChain] = None,
|
||||
album_info: Optional[MusicAlbumInfo] = None,
|
||||
media_source: Optional[MediaSource] = None,
|
||||
) -> _MusicScrapeFileResult:
|
||||
@@ -1306,7 +1323,7 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
cover: Optional[tuple[Optional[bytes], str]],
|
||||
lyrics_option: Optional[ScrapingOption],
|
||||
lyrics_overwrite: bool,
|
||||
lyrics_chain: Optional[LrclibChain],
|
||||
lyrics_chain: Optional[LyricsChain],
|
||||
album_info: Optional[MusicAlbumInfo],
|
||||
media_source: Optional[MediaSource],
|
||||
) -> _MusicScrapeFileResult:
|
||||
@@ -1495,14 +1512,14 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
scrape_info: Optional[MetaMusic | MusicInfo],
|
||||
lyrics_option: Optional[ScrapingOption],
|
||||
overwrite: bool,
|
||||
lyrics_chain: Optional[LrclibChain],
|
||||
lyrics_chain: Optional[LyricsChain],
|
||||
album_info: Optional[MusicAlbumInfo],
|
||||
) -> str:
|
||||
"""按歌词策略查询单个音轨并保存同名旁挂歌词文件。"""
|
||||
if not lyrics_option or lyrics_option.is_skip or not lyrics_chain:
|
||||
return "disabled"
|
||||
existing = self._find_music_lyrics_sidecar(fileitem)
|
||||
if existing and not overwrite:
|
||||
if existing and not overwrite and not getattr(lyrics_option, "is_upgrade", False):
|
||||
return "existing"
|
||||
if not scrape_info:
|
||||
return "missing"
|
||||
@@ -1511,11 +1528,23 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
if album_info:
|
||||
local_meta = AudioMetadataHelper.read(local_path)
|
||||
lookup_info = self._match_music_album_track(local_meta, album_info) or scrape_info
|
||||
lyrics = lyrics_chain.get_music_lyrics(lookup_info)
|
||||
embedded = AudioMetadataHelper.read_lyrics(local_path)
|
||||
lyrics = lyrics_chain.get_music_lyrics(
|
||||
lookup_info,
|
||||
local_candidates=[embedded] if embedded else None,
|
||||
)
|
||||
if lyrics_chain.budget_exceeded and not lyrics:
|
||||
return "budget_exceeded"
|
||||
if not lyrics or lyrics.instrumental or not lyrics.content or not lyrics.extension:
|
||||
return "missing"
|
||||
existing_quality = self._music_lyrics_sidecar_quality(existing)
|
||||
if existing_quality > lyrics.quality_rank:
|
||||
return "protected"
|
||||
if existing and existing_quality == lyrics.quality_rank and not overwrite:
|
||||
return "existing"
|
||||
status = "upgraded" if existing and lyrics.quality_rank > existing_quality else "saved"
|
||||
return (
|
||||
"saved"
|
||||
status
|
||||
if self._write_music_lyrics_sidecar(
|
||||
fileitem=fileitem,
|
||||
local_path=local_path,
|
||||
@@ -1532,14 +1561,34 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"""查找音轨旁已存在的同步或纯文本歌词文件。"""
|
||||
audio_path = Path(fileitem.path)
|
||||
for extension in self.MUSIC_LYRICS_EXTENSIONS:
|
||||
target_path = self._music_lyrics_path(audio_path, extension)
|
||||
item = self.storagechain.get_file_item(
|
||||
storage=fileitem.storage,
|
||||
path=audio_path.with_suffix(extension),
|
||||
path=target_path,
|
||||
)
|
||||
if item:
|
||||
return item
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _music_lyrics_path(cls, audio_path: Path, extension: str) -> Path:
|
||||
"""构造普通歌词和双扩展名 Lyricsfile 的同名旁挂路径。"""
|
||||
return audio_path.with_suffix(extension)
|
||||
|
||||
@staticmethod
|
||||
def _music_lyrics_sidecar_quality(fileitem: Optional[_SchemaFileItem]) -> int:
|
||||
"""按旁挂扩展名估算质量,用于写入前执行防降级保护。"""
|
||||
if not fileitem:
|
||||
return 0
|
||||
path = str(fileitem.path or "").casefold()
|
||||
if path.endswith(".lyricsfile.yaml"):
|
||||
return 4
|
||||
if path.endswith(".lrc"):
|
||||
return 3
|
||||
if path.endswith(".txt"):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def _write_music_lyrics_sidecar(
|
||||
self,
|
||||
fileitem: _SchemaFileItem,
|
||||
@@ -1581,6 +1630,13 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
):
|
||||
return False
|
||||
|
||||
if lyrics.lyricsfile and not self._write_music_lyricsfile_sidecar(
|
||||
fileitem=fileitem,
|
||||
local_path=local_path,
|
||||
content=lyrics.lyricsfile,
|
||||
):
|
||||
return False
|
||||
|
||||
if overwrite:
|
||||
self._remove_alternate_music_lyrics(fileitem, keep_extension=extension)
|
||||
return True
|
||||
@@ -1591,6 +1647,31 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
if temp_path and temp_path.exists() and temp_path != target_path:
|
||||
self._cleanup_temp_file(temp_path)
|
||||
|
||||
def _write_music_lyricsfile_sidecar(
|
||||
self,
|
||||
fileitem: _SchemaFileItem,
|
||||
local_path: Path,
|
||||
content: str,
|
||||
) -> bool:
|
||||
"""保留来源返回的标准 Lyricsfile,同时由主写入流程生成播放器兼容歌词。"""
|
||||
target_path = self._music_lyrics_path(Path(fileitem.path), ".lyricsfile.yaml")
|
||||
try:
|
||||
if fileitem.storage == "local":
|
||||
target_path.write_text(f"{content.rstrip()}\n", encoding="utf-8")
|
||||
return True
|
||||
parent = self.storagechain.get_parent_item(fileitem)
|
||||
if not parent:
|
||||
return False
|
||||
temp_path = local_path.with_suffix(".lyricsfile.yaml")
|
||||
temp_path.write_text(f"{content.rstrip()}\n", encoding="utf-8")
|
||||
try:
|
||||
return bool(self.storagechain.upload_file(parent, temp_path, new_name=target_path.name))
|
||||
finally:
|
||||
self._cleanup_temp_file(temp_path)
|
||||
except OSError as err:
|
||||
logger.warning(f"保存 Lyricsfile 失败:{target_path} - {err}")
|
||||
return False
|
||||
|
||||
def _remove_alternate_music_lyrics(
|
||||
self,
|
||||
fileitem: _SchemaFileItem,
|
||||
@@ -1599,11 +1680,12 @@ class ScrapingChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"""覆盖歌词格式后删除同音轨的旧扩展名文件,避免播放器优先读取过期内容。"""
|
||||
audio_path = Path(fileitem.path)
|
||||
for extension in self.MUSIC_LYRICS_EXTENSIONS:
|
||||
if extension == keep_extension:
|
||||
if extension in (keep_extension, ".lyricsfile.yaml"):
|
||||
continue
|
||||
target_path = self._music_lyrics_path(audio_path, extension)
|
||||
item = self.storagechain.get_file_item(
|
||||
storage=fileitem.storage,
|
||||
path=audio_path.with_suffix(extension),
|
||||
path=target_path,
|
||||
)
|
||||
if item and not self.storagechain.delete_file(item):
|
||||
logger.warning(f"删除旧歌词文件失败:{item.path}")
|
||||
|
||||
+36
-6
@@ -155,8 +155,10 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
self._subtitle_exts = self.runtime_config.subtitle_extensions
|
||||
# 音频文件后缀
|
||||
self._audio_exts = self.runtime_config.audio_extensions
|
||||
# 可处理的文件后缀(视频文件、字幕、音频文件)
|
||||
self._allowed_exts = self._media_exts + self._audio_exts + self._subtitle_exts
|
||||
# 可处理的文件后缀(视频文件、字幕、音频文件和音乐歌词)
|
||||
self._allowed_exts = self._media_exts + self._audio_exts + self._subtitle_exts + (
|
||||
".lrc", ".txt", ".yaml",
|
||||
)
|
||||
# 待整理任务队列
|
||||
self._queue = queue.Queue()
|
||||
# 文件整理线程
|
||||
@@ -1997,6 +1999,8 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
return False
|
||||
matched_template = True
|
||||
if batch_mtype == MediaType.MUSIC:
|
||||
if self._is_music_lyrics_file(item):
|
||||
return not self._is_blocked_by_exclude_words(item.path, exclude_words)
|
||||
if not self._is_media_file(item, batch_mtype):
|
||||
return False
|
||||
if not self._is_allow_filesize(item, min_filesize):
|
||||
@@ -2332,7 +2336,11 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
dir_key = self._get_file_parent_key(item)
|
||||
if not is_bluray_dir and self._is_media_file(item, batch_mtype):
|
||||
main_items_by_dir.setdefault(dir_key, []).append(item)
|
||||
elif self._is_subtitle_file(item) or self._is_audio_file(item):
|
||||
elif (
|
||||
self._is_subtitle_file(item)
|
||||
or self._is_audio_file(item)
|
||||
or self._is_music_lyrics_file(item)
|
||||
):
|
||||
extra_items_by_dir.setdefault(dir_key, []).append((item, is_bluray_dir))
|
||||
return main_items_by_dir, extra_items_by_dir
|
||||
|
||||
@@ -2358,7 +2366,11 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
if self._is_media_file(item, batch_mtype):
|
||||
main_fileitems.append(item)
|
||||
continue
|
||||
if not (self._is_subtitle_file(item) or self._is_audio_file(item)):
|
||||
if not (
|
||||
self._is_subtitle_file(item)
|
||||
or self._is_audio_file(item)
|
||||
or self._is_music_lyrics_file(item)
|
||||
):
|
||||
continue
|
||||
if not _is_allowed_transfer_item(item, False):
|
||||
continue
|
||||
@@ -2404,7 +2416,11 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
main_items = [(current_item, current_bluray_dir)]
|
||||
main_items_by_dir[current_dir_key] = [current_item]
|
||||
extra_items_by_dir[current_dir_key] = sibling_extra_items
|
||||
elif self._is_subtitle_file(current_item) or self._is_audio_file(current_item):
|
||||
elif (
|
||||
self._is_subtitle_file(current_item)
|
||||
or self._is_audio_file(current_item)
|
||||
or self._is_music_lyrics_file(current_item)
|
||||
):
|
||||
related_main_file_key = self._get_related_main_file_key(
|
||||
extra_fileitem=current_item,
|
||||
main_fileitems=sibling_main_items,
|
||||
@@ -2428,7 +2444,15 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
return list(items), inherited_map
|
||||
|
||||
if not main_items:
|
||||
return list(items), inherited_map
|
||||
remaining = [
|
||||
item
|
||||
for item in items
|
||||
if not (
|
||||
batch_mtype == MediaType.MUSIC
|
||||
and self._is_music_lyrics_file(item[0])
|
||||
)
|
||||
]
|
||||
return remaining, inherited_map
|
||||
|
||||
planned_items: List[Tuple[FileItem, bool]] = []
|
||||
seen_file_keys: set[Tuple[str, str]] = set()
|
||||
@@ -2516,6 +2540,12 @@ class TransferChain(FileFilterMixin, ScrapeBatchMixin, EpisodeFormatMixin, Histo
|
||||
inherited_map[self._get_file_key(extra_item)] = deepcopy(extra_meta)
|
||||
|
||||
for item, is_bluray_dir in items:
|
||||
if (
|
||||
batch_mtype == MediaType.MUSIC
|
||||
and self._is_music_lyrics_file(item)
|
||||
and self._get_file_key(item) not in inherited_map
|
||||
):
|
||||
continue
|
||||
_append_item(planned_items, seen_file_keys, item, is_bluray_dir)
|
||||
|
||||
return planned_items, inherited_map
|
||||
|
||||
Reference in New Issue
Block a user