194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
r"""NAS 폴더 스캐너 — 다운로드된 애니 목록 + 파일 정보 조회.
|
|
|
|
NAS Animation 폴더 구조:
|
|
\\192.168.10.10\NasData\Video\Animation\
|
|
[26_1분기]장송의프리렌2기\
|
|
[ASW] Sousou no Frieren S2 - 07.mkv
|
|
subtitles\...
|
|
[25_4분기]그노시아\
|
|
...
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import config
|
|
|
|
logger = logging.getLogger("variet.tools.nas")
|
|
|
|
|
|
@dataclass
|
|
class AnimeFolder:
|
|
"""NAS에 있는 애니 폴더 정보."""
|
|
folder_name: str # [26_1분기]장송의프리렌2기
|
|
full_path: str
|
|
title: str # 장송의프리렌2기
|
|
year: int # 26
|
|
quarter: int # 1
|
|
video_count: int = 0
|
|
subtitle_count: int = 0
|
|
total_size_gb: float = 0.0
|
|
video_files: list[str] = field(default_factory=list)
|
|
subtitle_files: list[str] = field(default_factory=list)
|
|
|
|
|
|
VIDEO_EXTS = {".mkv", ".mp4", ".avi", ".webm", ".m4v"}
|
|
SUB_EXTS = {".ass", ".srt", ".ssa", ".sub", ".smi"}
|
|
|
|
|
|
def _parse_folder_name(name: str) -> tuple[int, int, str]:
|
|
"""폴더명에서 연도, 분기, 제목 추출.
|
|
|
|
[26_1분기]장송의프리렌2기 → (26, 1, '장송의프리렌2기')
|
|
"""
|
|
m = re.match(r'\[(\d{2})_(\d)분기\](.+)', name)
|
|
if m:
|
|
return int(m.group(1)), int(m.group(2)), m.group(3)
|
|
return 0, 0, name
|
|
|
|
|
|
class NasScanner:
|
|
"""NAS Animation 폴더 스캐너."""
|
|
|
|
def __init__(self, base_path: str = ""):
|
|
self.base_path = Path(
|
|
base_path or getattr(config, "NAS_ANIME_PATH",
|
|
r"\\192.168.10.10\NasData\Video\Animation")
|
|
)
|
|
|
|
def is_accessible(self) -> bool:
|
|
"""NAS 접근 가능 여부."""
|
|
try:
|
|
return self.base_path.exists() and self.base_path.is_dir()
|
|
except (OSError, PermissionError):
|
|
return False
|
|
|
|
def list_anime_folders(
|
|
self,
|
|
year: Optional[int] = None,
|
|
quarter: Optional[int] = None,
|
|
) -> list[AnimeFolder]:
|
|
"""애니 폴더 목록 조회 (분기별 필터 가능)."""
|
|
if not self.is_accessible():
|
|
logger.error(f"NAS 경로 접근 불가: {self.base_path}")
|
|
return []
|
|
|
|
results = []
|
|
try:
|
|
for entry in sorted(self.base_path.iterdir()):
|
|
if not entry.is_dir():
|
|
continue
|
|
|
|
y, q, title = _parse_folder_name(entry.name)
|
|
|
|
# 필터링
|
|
if year is not None and y != year:
|
|
continue
|
|
if quarter is not None and q != quarter:
|
|
continue
|
|
|
|
folder = AnimeFolder(
|
|
folder_name=entry.name,
|
|
full_path=str(entry),
|
|
title=title,
|
|
year=y,
|
|
quarter=q,
|
|
)
|
|
|
|
# 파일 스캔
|
|
self._scan_folder(entry, folder)
|
|
results.append(folder)
|
|
except (OSError, PermissionError) as e:
|
|
logger.error(f"NAS 스캔 오류: {e}")
|
|
|
|
return results
|
|
|
|
def _scan_folder(self, path: Path, folder: AnimeFolder):
|
|
"""폴더 내 영상/자막 파일 집계."""
|
|
try:
|
|
for item in path.rglob("*"):
|
|
if not item.is_file():
|
|
continue
|
|
ext = item.suffix.lower()
|
|
size = item.stat().st_size
|
|
|
|
if ext in VIDEO_EXTS:
|
|
folder.video_count += 1
|
|
folder.video_files.append(item.name)
|
|
folder.total_size_gb += size / (1024 ** 3)
|
|
elif ext in SUB_EXTS:
|
|
folder.subtitle_count += 1
|
|
folder.subtitle_files.append(item.name)
|
|
except (OSError, PermissionError) as e:
|
|
logger.warning(f"파일 스캔 오류 ({path}): {e}")
|
|
|
|
def get_current_quarter_anime(self) -> list[AnimeFolder]:
|
|
"""이번 분기 다운로드된 애니 목록."""
|
|
from datetime import date
|
|
today = date.today()
|
|
year = today.year % 100
|
|
quarter = (today.month - 1) // 3 + 1
|
|
return self.list_anime_folders(year=year, quarter=quarter)
|
|
|
|
def search(self, keyword: str) -> list[AnimeFolder]:
|
|
"""키워드로 NAS 폴더 검색."""
|
|
all_folders = self.list_anime_folders()
|
|
kw = keyword.lower()
|
|
return [f for f in all_folders if kw in f.title.lower() or kw in f.folder_name.lower()]
|
|
|
|
def get_summary(self, year: Optional[int] = None, quarter: Optional[int] = None) -> dict:
|
|
"""요약 통계."""
|
|
folders = self.list_anime_folders(year=year, quarter=quarter)
|
|
return {
|
|
"total_anime": len(folders),
|
|
"total_videos": sum(f.video_count for f in folders),
|
|
"total_subtitles": sum(f.subtitle_count for f in folders),
|
|
"total_size_gb": round(sum(f.total_size_gb for f in folders), 2),
|
|
"folders": folders,
|
|
}
|
|
|
|
|
|
# ── CLI 진입점 ──
|
|
if __name__ == "__main__":
|
|
import sys
|
|
import json
|
|
|
|
scanner = NasScanner()
|
|
args = sys.argv[1:]
|
|
|
|
if not args or args[0] == "scan":
|
|
# python tools/nas_scanner.py scan [--year 26] [--quarter 1]
|
|
year = quarter = None
|
|
for i, a in enumerate(args):
|
|
if a == "--year" and i + 1 < len(args):
|
|
year = int(args[i + 1])
|
|
if a == "--quarter" and i + 1 < len(args):
|
|
quarter = int(args[i + 1])
|
|
|
|
folders = scanner.list_anime_folders(year=year, quarter=quarter)
|
|
for f in folders:
|
|
print(f"📁 {f.folder_name} | 영상 {f.video_count}개 | 자막 {f.subtitle_count}개 | {f.total_size_gb:.1f}GB")
|
|
print(f"\n총 {len(folders)}개 애니, 영상 {sum(f.video_count for f in folders)}개")
|
|
|
|
elif args[0] == "search" and len(args) > 1:
|
|
# python tools/nas_scanner.py search "프리렌"
|
|
keyword = " ".join(args[1:])
|
|
results = scanner.search(keyword)
|
|
for f in results:
|
|
print(f"📁 {f.folder_name} | 영상 {f.video_count}개 | 자막 {f.subtitle_count}개")
|
|
if not results:
|
|
print(f"'{keyword}' 검색 결과 없음")
|
|
|
|
elif args[0] == "summary":
|
|
# python tools/nas_scanner.py summary
|
|
summary = scanner.get_summary()
|
|
print(json.dumps(summary, ensure_ascii=False, indent=2, default=str))
|
|
|
|
else:
|
|
print("사용법: python tools/nas_scanner.py [scan|search|summary] [옵션]")
|
|
|