Compare commits
4 Commits
linux_rele
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a4e0ae0b08 | |||
| 178776d307 | |||
| cad3fc8117 | |||
| c54fa70012 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,3 +9,6 @@ __pycache__/
|
||||
metadata_1.json
|
||||
metadata_mi.json
|
||||
main.spec
|
||||
.vscode/settings.json
|
||||
build/
|
||||
dist/
|
||||
|
||||
@@ -232,10 +232,9 @@ def ExtractZIP(zip_file: str, extract_to: str):
|
||||
|
||||
#
|
||||
def CreateZIP(output_zip: str, files: list[str]) -> bool:
|
||||
with zipfile.ZipFile(output_zip, 'w') as zf:
|
||||
with zipfile.ZipFile(output_zip, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for file in files:
|
||||
pathTemp = os.path.join('root', os.path.basename(file))
|
||||
zf.write(file, pathTemp)
|
||||
zf.write(file, os.path.basename(file))
|
||||
|
||||
bRet = False
|
||||
if os.path.exists(output_zip):
|
||||
|
||||
307
main.py
307
main.py
@@ -39,6 +39,8 @@ def main(argc, argv):
|
||||
parser.add_argument('--print', dest='bPrint', action='store_true', help='comicinfo.xml 출력')
|
||||
parser.add_argument('--metadata', dest='bMetadata', action='store_true', help='.metadata 파일로부터 ComicInfo.xml 생성 및 CBZ에 추가')
|
||||
parser.add_argument('--verbose', dest='bVerbose', action='store_true', help='상세 로그 출력')
|
||||
parser.add_argument('--fix-arc-path', dest='bFixArcPath', action='store_true', help='CBZ 아카이브의 내부 폴더 구조를 제거합니다.')
|
||||
parser.add_argument('--fix-xml', dest='bFixXml', action='store_true', help='ComicInfo.xml의 특수문자(&) 이스케이프 및 문법 오류를 검사하고 수정합니다.')
|
||||
parser.add_argument('-v', '--version', action='version', version='ComicInfoXMLConv 1.0')
|
||||
|
||||
args = parser.parse_args(argv[1:]) # argv[0]은 스크립트 이름 제외
|
||||
@@ -52,68 +54,13 @@ def main(argc, argv):
|
||||
# 태그 파싱
|
||||
if args.info_tags is not None and len(args.info_tags) > 0:
|
||||
util.DbgOut(f"입력된 태그: {args.info_tags}", True)
|
||||
listTags = args.info_tags.split(',')
|
||||
util.DbgOut(f"태그 리스트: {listTags}", True)
|
||||
# listTags = args.info_tags.split(',') # This variable is not used
|
||||
# util.DbgOut(f"태그 리스트: {listTags}", True)
|
||||
|
||||
# 단일 파일 처리 (--in 옵션)
|
||||
if True == util.IsExistsPath(args.arc_file):
|
||||
util.DbgOut(f"대상 파일: {args.arc_file}", True)
|
||||
|
||||
# Path 변수 확장
|
||||
arc_title = ExpandPathVars(args.info_title, args.arc_file)
|
||||
arc_writer = ExpandPathVars(args.info_writer, args.arc_file)
|
||||
arc_series = ExpandPathVars(args.info_series, args.arc_file)
|
||||
arc_volume = ExpandPathVars(args.info_volume, args.arc_file)
|
||||
arc_number = ExpandPathVars(args.info_number, args.arc_file)
|
||||
arc_tags = args.info_tags.split(',') if args.info_tags else []
|
||||
|
||||
# .metadata 옵션이 활성화된 경우
|
||||
if True == args.bMetadata:
|
||||
ProcessMetadataToCBZ(args.arc_file, args.bPrint,
|
||||
arc_volume, arc_number, args.info_count,
|
||||
arc_title, arc_writer, arc_series,
|
||||
arc_tags if arc_tags else None)
|
||||
return
|
||||
|
||||
listArcContents = util.GetZipContentList(args.arc_file)
|
||||
|
||||
if True == any(item.lower() == "comicinfo.xml" for item in listArcContents):
|
||||
util.DbgOut(f"comicinfo.xml 발견됨: {args.arc_file}", True)
|
||||
|
||||
comicinfo_xml_bytes = util.GetZippedFileByte(args.arc_file, "ComicInfo.xml")
|
||||
if None != comicinfo_xml_bytes:
|
||||
modified = ModComicInfoXML(comicinfo_xml_bytes, arc_title, arc_writer, arc_series, arc_tags,
|
||||
info_number=arc_number, info_volume=arc_volume, info_count=args.info_count)
|
||||
|
||||
if True == args.bPrint:
|
||||
if modified is not None:
|
||||
print(modified.decode('utf-8'))
|
||||
else:
|
||||
print(comicinfo_xml_bytes.decode('utf-8'))
|
||||
elif modified is not None:
|
||||
util.AddFileToZip(args.arc_file, "ComicInfo.xml", modified)
|
||||
util.DbgOut(f"ComicInfo.xml 업데이트됨: {args.arc_file}", True)
|
||||
|
||||
else:
|
||||
util.DbgOut(f"comicinfo.xml 없음: {args.arc_file}", True)
|
||||
if arc_volume or arc_number or arc_title or arc_writer or arc_series or arc_tags or args.info_count:
|
||||
new_xml = CreateComicInfoXML(
|
||||
info_title=arc_title if arc_title else "",
|
||||
info_writer=arc_writer if arc_writer else "",
|
||||
info_series=arc_series if arc_series else "",
|
||||
tags=arc_tags if arc_tags else [],
|
||||
pages=[],
|
||||
volume=arc_volume if arc_volume else "",
|
||||
number=arc_number if arc_number else "",
|
||||
count=int(args.info_count) if args.info_count is not None else -1
|
||||
)
|
||||
if new_xml:
|
||||
if True == args.bPrint:
|
||||
print(new_xml)
|
||||
else:
|
||||
util.AddFileToZip(args.arc_file, "ComicInfo.xml", new_xml.encode('utf-8'))
|
||||
util.DbgOut(f"ComicInfo.xml 생성됨: {args.arc_file}", True)
|
||||
|
||||
process_archive_file(args.arc_file, args)
|
||||
return
|
||||
|
||||
# 폴더 처리 (--root 옵션) - 하위 폴더 포함, CBZ 파일만 처리
|
||||
@@ -127,66 +74,101 @@ def main(argc, argv):
|
||||
continue
|
||||
|
||||
util.DbgOut(f"파일 발견: {strFullPath}", True)
|
||||
|
||||
# Path 변수 확장 (파일마다 달라짐)
|
||||
arc_title = ExpandPathVars(args.info_title, strFullPath)
|
||||
arc_writer = ExpandPathVars(args.info_writer, strFullPath)
|
||||
arc_series = ExpandPathVars(args.info_series, strFullPath)
|
||||
arc_volume = ExpandPathVars(args.info_volume, strFullPath)
|
||||
arc_number = ExpandPathVars(args.info_number, strFullPath)
|
||||
arc_tags = args.info_tags.split(',') if args.info_tags else []
|
||||
|
||||
# .metadata 옵션이 활성화된 경우
|
||||
if True == args.bMetadata:
|
||||
ProcessMetadataToCBZ(strFullPath, args.bPrint,
|
||||
arc_volume, arc_number, args.info_count,
|
||||
arc_title, arc_writer, arc_series,
|
||||
arc_tags if arc_tags else None)
|
||||
continue
|
||||
|
||||
listArcContents = util.GetZipContentList(strFullPath)
|
||||
|
||||
if True == any(item.lower() == "comicinfo.xml" for item in listArcContents):
|
||||
util.DbgOut(f"comicinfo.xml 발견됨: {strFullPath}", True)
|
||||
|
||||
comicinfo_xml_bytes = util.GetZippedFileByte(strFullPath, "ComicInfo.xml")
|
||||
if None != comicinfo_xml_bytes:
|
||||
modified = ModComicInfoXML(comicinfo_xml_bytes, arc_title, arc_writer, arc_series, arc_tags,
|
||||
info_number=arc_number, info_volume=arc_volume, info_count=args.info_count)
|
||||
|
||||
if True == args.bPrint:
|
||||
if modified is not None:
|
||||
print(modified.decode('utf-8'))
|
||||
else:
|
||||
print(comicinfo_xml_bytes.decode('utf-8'))
|
||||
elif modified is not None:
|
||||
util.AddFileToZip(strFullPath, "ComicInfo.xml", modified)
|
||||
util.DbgOut(f"ComicInfo.xml 업데이트됨: {strFullPath}", True)
|
||||
|
||||
else:
|
||||
util.DbgOut(f"comicinfo.xml 없음: {strFullPath}", True)
|
||||
if arc_volume or arc_number or arc_title or arc_writer or arc_series or arc_tags or args.info_count:
|
||||
new_xml = CreateComicInfoXML(
|
||||
info_title=arc_title if arc_title else "",
|
||||
info_writer=arc_writer if arc_writer else "",
|
||||
info_series=arc_series if arc_series else "",
|
||||
tags=arc_tags if arc_tags else [],
|
||||
pages=[],
|
||||
volume=arc_volume if arc_volume else "",
|
||||
number=arc_number if arc_number else "",
|
||||
count=int(args.info_count) if args.info_count is not None else -1
|
||||
)
|
||||
if new_xml:
|
||||
if True == args.bPrint:
|
||||
print(new_xml)
|
||||
else:
|
||||
util.AddFileToZip(strFullPath, "ComicInfo.xml", new_xml.encode('utf-8'))
|
||||
util.DbgOut(f"ComicInfo.xml 생성됨: {strFullPath}", True)
|
||||
|
||||
process_archive_file(strFullPath, args)
|
||||
|
||||
return
|
||||
|
||||
#
|
||||
def process_archive_file(archive_path: str, args: argparse.Namespace):
|
||||
"""지정된 아카이브 파일에 대해 ComicInfo.xml을 처리합니다."""
|
||||
# --fix-arc-path 옵션 처리
|
||||
if args.bFixArcPath:
|
||||
if FlattenZipArchive(archive_path):
|
||||
util.DbgOut(f"아카이브 폴더 구조 제거 완료: {archive_path}", True)
|
||||
|
||||
# Path 변수 확장
|
||||
arc_title = ExpandPathVars(args.info_title, archive_path)
|
||||
arc_writer = ExpandPathVars(args.info_writer, archive_path)
|
||||
arc_series = ExpandPathVars(args.info_series, archive_path)
|
||||
arc_volume = ExpandPathVars(args.info_volume, archive_path)
|
||||
arc_number = ExpandPathVars(args.info_number, archive_path)
|
||||
arc_tags = args.info_tags.split(',') if args.info_tags else []
|
||||
|
||||
# info_count를 int로 변환 (argparse에서 str로 파싱됨)
|
||||
arc_count = int(args.info_count) if args.info_count is not None else None
|
||||
|
||||
# .metadata 옵션이 활성화된 경우
|
||||
if args.bMetadata:
|
||||
ProcessMetadataToCBZ(archive_path, args.bPrint,
|
||||
arc_volume, arc_number, arc_count,
|
||||
arc_title, arc_writer, arc_series,
|
||||
arc_tags if arc_tags else None)
|
||||
return
|
||||
|
||||
listArcContents = util.GetZipContentList(archive_path)
|
||||
|
||||
comicinfo_path_in_zip = None
|
||||
for item in listArcContents:
|
||||
# os.path.basename은 '/' 또는 '\' 구분자를 사용하여 경로의 마지막 부분을 반환합니다.
|
||||
if os.path.basename(item.lower()) == "comicinfo.xml":
|
||||
comicinfo_path_in_zip = item
|
||||
break
|
||||
has_comicinfo = comicinfo_path_in_zip is not None
|
||||
|
||||
if has_comicinfo:
|
||||
util.DbgOut(f"comicinfo.xml 발견됨: {archive_path} (경로: {comicinfo_path_in_zip})", True)
|
||||
comicinfo_xml_bytes = util.GetZippedFileByte(archive_path, comicinfo_path_in_zip)
|
||||
if comicinfo_xml_bytes:
|
||||
fixed_xml = None
|
||||
if args.bFixXml:
|
||||
fixed_xml, msgs = ValidateAndFixComicInfoXML(comicinfo_xml_bytes)
|
||||
for msg in msgs:
|
||||
util.DbgOut(msg, True)
|
||||
if fixed_xml is not None:
|
||||
comicinfo_xml_bytes = fixed_xml
|
||||
|
||||
has_mods = any([arc_title, arc_writer, arc_series, arc_tags,
|
||||
arc_number, arc_volume, arc_count])
|
||||
|
||||
if not has_mods:
|
||||
if fixed_xml is not None:
|
||||
util.AddFileToZip(archive_path, comicinfo_path_in_zip, fixed_xml)
|
||||
util.DbgOut(f"ComicInfo.xml 특수문자 수정 완료: {archive_path}", True)
|
||||
if args.bPrint:
|
||||
print(comicinfo_xml_bytes.decode('utf-8'))
|
||||
return
|
||||
|
||||
modified = ModComicInfoXML(comicinfo_xml_bytes, arc_title, arc_writer, arc_series, arc_tags,
|
||||
info_number=arc_number, info_volume=arc_volume, info_count=arc_count)
|
||||
|
||||
if args.bPrint:
|
||||
output = modified.decode('utf-8') if modified else comicinfo_xml_bytes.decode('utf-8')
|
||||
print(output)
|
||||
elif modified:
|
||||
util.AddFileToZip(archive_path, comicinfo_path_in_zip, modified)
|
||||
util.DbgOut(f"ComicInfo.xml 업데이트됨: {archive_path} (경로: {comicinfo_path_in_zip})", True)
|
||||
elif fixed_xml is not None:
|
||||
util.AddFileToZip(archive_path, comicinfo_path_in_zip, fixed_xml)
|
||||
util.DbgOut(f"ComicInfo.xml 특수문자 수정 완료: {archive_path}", True)
|
||||
else:
|
||||
util.DbgOut(f"comicinfo.xml 없음: {archive_path}", True)
|
||||
if arc_volume or arc_number or arc_title or arc_writer or arc_series or arc_tags or arc_count is not None:
|
||||
new_xml = CreateComicInfoXML(
|
||||
info_title=arc_title or "",
|
||||
info_writer=arc_writer or "",
|
||||
info_series=arc_series or "",
|
||||
tags=arc_tags,
|
||||
pages=[],
|
||||
volume=arc_volume or "",
|
||||
number=arc_number or "",
|
||||
count=arc_count if arc_count is not None else -1
|
||||
)
|
||||
if new_xml:
|
||||
if args.bPrint:
|
||||
print(new_xml)
|
||||
else:
|
||||
util.AddFileToZip(archive_path, "ComicInfo.xml", new_xml.encode('utf-8'))
|
||||
util.DbgOut(f"ComicInfo.xml 생성됨: {archive_path}", True)
|
||||
|
||||
def ExpandPathVars(text: str | None, filepath: str) -> str | None:
|
||||
"""Replace path template variables (<filename>, <parentfolder>, etc.) with actual values.
|
||||
After variable expansion, if the result has a :s/pattern/replacement/ suffix,
|
||||
@@ -194,7 +176,9 @@ def ExpandPathVars(text: str | None, filepath: str) -> str | None:
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
dirpath = os.path.dirname(filepath)
|
||||
# 경로 정규화: ./ 또는 ../ 등 상대 경로 표현을 제거하여 정확한 폴더명 추출
|
||||
normpath = os.path.normpath(filepath)
|
||||
dirpath = os.path.dirname(normpath)
|
||||
cur_folder = os.path.basename(dirpath) if dirpath else ""
|
||||
par_folder = os.path.basename(os.path.dirname(dirpath)) if dirpath and os.path.dirname(dirpath) else ""
|
||||
filename_no_ext = os.path.splitext(os.path.basename(filepath))[0]
|
||||
@@ -202,7 +186,6 @@ def ExpandPathVars(text: str | None, filepath: str) -> str | None:
|
||||
|
||||
var_map = {
|
||||
"filename": filename_no_ext,
|
||||
"parentfolder": cur_folder,
|
||||
"currentfolder": cur_folder,
|
||||
"parentfolder": par_folder,
|
||||
"fullpath": filepath,
|
||||
@@ -215,8 +198,8 @@ def ExpandPathVars(text: str | None, filepath: str) -> str | None:
|
||||
for key, value in var_map.items():
|
||||
result = result.replace(f"<{key}>", value)
|
||||
|
||||
# After variable expansion, check for :s/pattern/replacement/ suffix
|
||||
m = re.match(r'^(.*):s(.)(.+?)\2(.*?)(?:\2([ig]*))?$', result, re.DOTALL)
|
||||
# After variable expansion, check for :s/pattern/replacement/ or s/pattern/replacement/ suffix
|
||||
m = re.match(r'^(.*):?s(.)(.+?)\2(.*?)(?:\2([ig]*))?$', result, re.DOTALL)
|
||||
if m:
|
||||
source = m.group(1)
|
||||
delim = m.group(2)
|
||||
@@ -228,13 +211,57 @@ def ExpandPathVars(text: str | None, filepath: str) -> str | None:
|
||||
if 'i' in flags_str:
|
||||
flags |= re.IGNORECASE
|
||||
count = 0 if 'g' in flags_str else 1
|
||||
try:
|
||||
result = re.sub(pattern, replacement_safe, source, count=count, flags=flags)
|
||||
except re.error:
|
||||
pass
|
||||
# source가 비어있으면 정규식 미적용 (ResolveRegexTransform에서 처리)
|
||||
if source:
|
||||
try:
|
||||
result = re.sub(pattern, replacement_safe, source, count=count, flags=flags)
|
||||
except re.error:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def FlattenZipArchive(zip_path: str) -> bool:
|
||||
"""
|
||||
ZIP 아카이브의 내부 폴더 구조를 제거합니다.
|
||||
모든 파일을 루트 디렉토리로 이동시킵니다.
|
||||
|
||||
Args:
|
||||
zip_path: ZIP 파일 경로
|
||||
|
||||
Returns:
|
||||
구조가 변경되었으면 True, 아니면 False
|
||||
"""
|
||||
try:
|
||||
needs_flattening = False
|
||||
with util.zipfile.ZipFile(zip_path, 'r') as zin:
|
||||
for item in zin.infolist():
|
||||
if '/' in item.filename or '\\' in item.filename:
|
||||
needs_flattening = True
|
||||
break
|
||||
|
||||
if not needs_flattening:
|
||||
return False
|
||||
|
||||
temp_zip = zip_path + '.tmp'
|
||||
|
||||
with util.zipfile.ZipFile(zip_path, 'r') as zin:
|
||||
with util.zipfile.ZipFile(temp_zip, 'w') as zout:
|
||||
for item in zin.infolist():
|
||||
if item.is_dir():
|
||||
continue
|
||||
data = zin.read(item.filename)
|
||||
new_name = os.path.basename(item.filename)
|
||||
zout.writestr(new_name, data)
|
||||
|
||||
util.shutil.move(temp_zip, zip_path)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
util.DbgOut(f"아카이브 구조 변경 중 오류 발생: {e}", True)
|
||||
if 'temp_zip' in locals() and os.path.exists(temp_zip):
|
||||
os.remove(temp_zip)
|
||||
return False
|
||||
|
||||
|
||||
def ResolveRegexTransform(text: str | None, current_value: str | None) -> str | None:
|
||||
"""Apply regex substitution if text uses s/pattern/replacement/ syntax.
|
||||
@@ -258,6 +285,38 @@ def ResolveRegexTransform(text: str | None, current_value: str | None) -> str |
|
||||
return text
|
||||
|
||||
|
||||
def ValidateAndFixComicInfoXML(xml_bytes: bytes) -> tuple[bytes | None, list[str]]:
|
||||
try:
|
||||
ET.fromstring(xml_bytes)
|
||||
return (None, ["XML is well-formed."])
|
||||
except ET.ParseError as e:
|
||||
msgs = [f"Parse error: {e}"]
|
||||
|
||||
try:
|
||||
text = xml_bytes.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
msgs.append("Cannot decode XML (expected UTF-8).")
|
||||
return (None, msgs)
|
||||
|
||||
text, count = re.subn(
|
||||
r'&(?!(?:amp|lt|gt|quot|apos);|#\d+;|#x[0-9a-fA-F]+;)',
|
||||
'&',
|
||||
text
|
||||
)
|
||||
if count > 0:
|
||||
msgs.append(f"Fixed {count} unescaped & character(s).")
|
||||
fixed_bytes = text.encode('utf-8')
|
||||
try:
|
||||
ET.fromstring(fixed_bytes)
|
||||
msgs.append("XML is now well-formed.")
|
||||
except ET.ParseError as e2:
|
||||
msgs.append(f"Remaining issues after fix: {e2}")
|
||||
return (fixed_bytes, msgs)
|
||||
|
||||
msgs.append("No fixable issues found.")
|
||||
return (None, msgs)
|
||||
|
||||
|
||||
def ParsePathForComicInfo(strFullPath: str) -> dict:
|
||||
"""
|
||||
파일 경로에서 작가, 제목, 번호를 추출합니다.
|
||||
@@ -443,7 +502,7 @@ def ConvertLanguageToISO(lang_name: str) -> str:
|
||||
|
||||
#
|
||||
def ProcessMetadataToCBZ(strArcPath: str, bPrint: bool = False,
|
||||
volume: str = "-1", number: str = "", count: int = -1,
|
||||
volume: str = "-1", number: str = "", count: int | None = None,
|
||||
info_title: str = None, info_writer: str = None,
|
||||
info_series: str = None, tags: list[str] = None) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user