initialize repository
This commit is contained in:
767
main.py
Executable file
767
main.py
Executable file
@@ -0,0 +1,767 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import argparse
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
import re
|
||||
import json
|
||||
|
||||
import UtilPack as util
|
||||
|
||||
# ComicInfo.xsd에서 파싱한 default 값 딕셔너리
|
||||
DEFAULT_VALUES = {
|
||||
'Count': '-1',
|
||||
'Volume': '',
|
||||
'Number': '',
|
||||
'Series': '',
|
||||
'Title': '',
|
||||
'Writer': '',
|
||||
'LanguageISO': '',
|
||||
'Manga': 'Yes',
|
||||
'Pages': None, # 특수 처리
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
def main(argc, argv):
|
||||
parser = argparse.ArgumentParser(description="ComicInfo.XML 생성기")
|
||||
parser.add_argument('--root', dest='root_dir', required=False, help='입력 폴더 지정')
|
||||
parser.add_argument('--in', dest='arc_file', required=False, help='대상 파일 지정')
|
||||
parser.add_argument('--title', dest='info_title', required=False, help='제목')
|
||||
parser.add_argument('--author', dest='info_writer', required=False, help='작가')
|
||||
parser.add_argument('--series', dest='info_series', required=False, help='시리즈')
|
||||
parser.add_argument('--tags', dest='info_tags', required=False, help='태그 (쉼표로 구분)')
|
||||
parser.add_argument('--volume', dest='info_volume', required=False, help='권/화 묶음 번호 (예: 3)')
|
||||
parser.add_argument('--number', dest='info_number', required=False, help='개별 권/화 번호 (예: 1)')
|
||||
parser.add_argument('--count', dest='info_count', required=False, help='시리즈 총 권수/화수 (기본값: -1)')
|
||||
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('-v', '--version', action='version', version='ComicInfoXMLConv 1.0')
|
||||
|
||||
args = parser.parse_args(argv[1:]) # argv[0]은 스크립트 이름 제외
|
||||
|
||||
# verbose 모드 설정
|
||||
util.SetVerbose(args.bVerbose)
|
||||
|
||||
if True == args.bVerbose:
|
||||
util.DbgOut("환영합니다! ComicInfo.XML 생성기를 시작합니다.", True)
|
||||
|
||||
# 태그 파싱
|
||||
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)
|
||||
|
||||
# 단일 파일 처리 (--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)
|
||||
|
||||
return
|
||||
|
||||
# 폴더 처리 (--root 옵션) - 하위 폴더 포함, CBZ 파일만 처리
|
||||
if True == util.IsExistsPath(args.root_dir):
|
||||
util.DbgOut(f"입력 폴더: {args.root_dir}", True)
|
||||
# CBZ 확장자 파일만 리커시브로 검색
|
||||
listArcFiles = util.ListFileExtRcr(args.root_dir, ['cbz'])
|
||||
|
||||
for strFullPath in listArcFiles:
|
||||
if False == os.path.exists(strFullPath):
|
||||
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)
|
||||
|
||||
|
||||
return
|
||||
|
||||
#
|
||||
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,
|
||||
the regex is applied to the expanded text.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
dirpath = os.path.dirname(filepath)
|
||||
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]
|
||||
ext = os.path.splitext(filepath)[1]
|
||||
|
||||
var_map = {
|
||||
"filename": filename_no_ext,
|
||||
"parentfolder": cur_folder,
|
||||
"currentfolder": cur_folder,
|
||||
"parentfolder": par_folder,
|
||||
"fullpath": filepath,
|
||||
"ext": ext,
|
||||
}
|
||||
|
||||
result = text
|
||||
|
||||
# Plain variable replacement
|
||||
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)
|
||||
if m:
|
||||
source = m.group(1)
|
||||
delim = m.group(2)
|
||||
pattern = m.group(3)
|
||||
replacement = m.group(4)
|
||||
flags_str = m.group(5) or ''
|
||||
replacement_safe = re.sub(r'\$(\d+)', r'\\\1', replacement)
|
||||
flags = 0
|
||||
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
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def ResolveRegexTransform(text: str | None, current_value: str | None) -> str | None:
|
||||
"""Apply regex substitution if text uses s/pattern/replacement/ syntax.
|
||||
Returns the transformed value, or the original text if no regex pattern detected."""
|
||||
if not text or current_value is None:
|
||||
return text
|
||||
m = re.match(r'^s(.)(.+?)\1(.*?)(?:\1([ig]*))?$', text, re.DOTALL)
|
||||
if m:
|
||||
pattern = m.group(2)
|
||||
replacement = m.group(3)
|
||||
replacement = re.sub(r'\$(\d+)', r'\\\1', replacement)
|
||||
flags_str = m.group(4) or ''
|
||||
flags = 0
|
||||
if 'i' in flags_str:
|
||||
flags |= re.IGNORECASE
|
||||
count = 0 if 'g' in flags_str else 1
|
||||
try:
|
||||
return re.sub(pattern, replacement, current_value, count=count, flags=flags)
|
||||
except re.error:
|
||||
return text
|
||||
return text
|
||||
|
||||
|
||||
def ParsePathForComicInfo(strFullPath: str) -> dict:
|
||||
"""
|
||||
파일 경로에서 작가, 제목, 번호를 추출합니다.
|
||||
예: /some/path/[Kim] Some Comic/blablabla_01.cbz
|
||||
반환: {'author': 'Kim', 'title': 'Some Comic', 'number': '01'}
|
||||
|
||||
Args:
|
||||
strFullPath: 파일 전체 경로
|
||||
|
||||
Returns:
|
||||
{'author': str, 'title': str, 'number': str} 딕셔너리
|
||||
"""
|
||||
result = {
|
||||
'author': None,
|
||||
'title': None,
|
||||
'number': None
|
||||
}
|
||||
|
||||
try:
|
||||
# 경로를 / 로 분리
|
||||
path_parts = strFullPath.replace('\\', '/').split('/')
|
||||
|
||||
# [] 가 포함된 부분 찾기
|
||||
folder_with_bracket = None
|
||||
for part in path_parts:
|
||||
if '[' in part and ']' in part:
|
||||
folder_with_bracket = part
|
||||
break
|
||||
|
||||
# [] 로 분리해서 작가와 제목 추출
|
||||
if folder_with_bracket:
|
||||
# [작가] 제목 형식으로 분리
|
||||
match = re.match(r'\[([^\]]+)\]\s*(.+)', folder_with_bracket)
|
||||
if match:
|
||||
result['author'] = match.group(1).strip()
|
||||
result['title'] = match.group(2).strip()
|
||||
|
||||
# 파일명에서 번호 추출 (마지막 _ 다음 숫자)
|
||||
filename = path_parts[-1] # 파일명 (blablabla_01.cbz)
|
||||
|
||||
# _숫자 형식 찾기
|
||||
number_match = re.search(r'_(\d+)', filename)
|
||||
if number_match:
|
||||
result['number'] = number_match.group(1)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
util.DbgOut(f"경로 파싱 오류: {e}", True)
|
||||
return result
|
||||
|
||||
#
|
||||
def GetComicInfoXMLfromARC(strArcPath) -> bytes:
|
||||
# zip 파일에서 comicinfo.xml을 추출하는 로직을 여기에 구현
|
||||
# 예시로, zip 파일에서 comicinfo.xml을 읽어와서 bytes 형태로 반환하는 과정을 생각할 수 있습니다.
|
||||
listContents = util.GetZipContentList(strArcPath)
|
||||
|
||||
for item in listContents:
|
||||
if item.lower() == "comicinfo.xml":
|
||||
return util.GetZippedFileByte(strArcPath, item)
|
||||
|
||||
return None
|
||||
|
||||
#
|
||||
def ModComicInfoXMLFile(strXMLPath, info_title, info_writer, info_series, tags: list) -> bool:
|
||||
if False == util.IsExistsPath(strXMLPath):
|
||||
util.DbgOut(f"comicinfo.xml 파일이 존재하지 않습니다: {strXMLPath}", True)
|
||||
return False
|
||||
|
||||
with open(strXMLPath, "rb") as f:
|
||||
xml_bytes = f.read()
|
||||
|
||||
modified = ModComicInfoXML(xml_bytes, info_title, info_writer, info_series, tags)
|
||||
if modified is not None:
|
||||
with open(strXMLPath, "wb") as f:
|
||||
f.write(modified)
|
||||
return True
|
||||
return False
|
||||
|
||||
#
|
||||
def ModComicInfoXML(pBytes, info_title, info_writer, info_series, tags: list,
|
||||
info_number=None, info_volume=None, info_count=None) -> bytes | None:
|
||||
try:
|
||||
root = ET.fromstring(pBytes)
|
||||
|
||||
cur_title = root.findtext('Title') if root.find('Title') is not None else ""
|
||||
cur_writer = root.findtext('Writer') if root.find('Writer') is not None else ""
|
||||
cur_series = root.findtext('Series') if root.find('Series') is not None else ""
|
||||
|
||||
info_title = ResolveRegexTransform(info_title, cur_title)
|
||||
info_writer = ResolveRegexTransform(info_writer, cur_writer)
|
||||
info_series = ResolveRegexTransform(info_series, cur_series)
|
||||
|
||||
if info_title is not None and info_title != "":
|
||||
title_elem = root.find('Title')
|
||||
if title_elem is None:
|
||||
title_elem = ET.SubElement(root, 'Title')
|
||||
title_elem.text = info_title
|
||||
|
||||
if info_writer is not None and info_writer != "":
|
||||
writer_elem = root.find('Writer')
|
||||
if writer_elem is None:
|
||||
writer_elem = ET.SubElement(root, 'Writer')
|
||||
writer_elem.text = info_writer
|
||||
|
||||
if info_series is not None and info_series != "":
|
||||
series_elem = root.find('Series')
|
||||
if series_elem is None:
|
||||
series_elem = ET.SubElement(root, 'Series')
|
||||
series_elem.text = info_series
|
||||
|
||||
if tags is not None and len(tags) > 0:
|
||||
tags_elem = root.find('Tags')
|
||||
if tags_elem is not None:
|
||||
root.remove(tags_elem)
|
||||
tags_elem = ET.SubElement(root, 'Tags')
|
||||
tags_elem.text = ','.join(tags)
|
||||
|
||||
if info_number is not None and info_number != "" and info_number != "-1":
|
||||
number_elem = root.find('Number')
|
||||
if number_elem is None:
|
||||
number_elem = ET.SubElement(root, 'Number')
|
||||
number_elem.text = str(info_number)
|
||||
|
||||
if info_volume is not None and info_volume != "" and info_volume != "-1":
|
||||
volume_elem = root.find('Volume')
|
||||
if volume_elem is None:
|
||||
volume_elem = ET.SubElement(root, 'Volume')
|
||||
volume_elem.text = str(info_volume)
|
||||
|
||||
if info_count is not None and info_count != -1:
|
||||
count_elem = root.find('Count')
|
||||
if count_elem is None:
|
||||
count_elem = ET.SubElement(root, 'Count')
|
||||
count_elem.text = str(info_count)
|
||||
|
||||
output = BytesIO()
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(output, encoding='utf-8', xml_declaration=True)
|
||||
|
||||
modified = output.getvalue()
|
||||
if modified != pBytes:
|
||||
return modified
|
||||
return None
|
||||
|
||||
except ET.ParseError as e:
|
||||
util.DbgOut(f"XML 파싱 오류: {e}", True)
|
||||
return None
|
||||
except Exception as e:
|
||||
util.DbgOut(f"ComicInfo.xml 수정 중 오류 발생: {e}", True)
|
||||
return None
|
||||
|
||||
#
|
||||
def ConvertLanguageToISO(lang_name: str) -> str:
|
||||
"""
|
||||
언어 이름을 ISO 639-1 코드로 변환합니다.
|
||||
|
||||
Args:
|
||||
lang_name: 언어 이름 (예: "korean", "japanese", "english")
|
||||
|
||||
Returns:
|
||||
ISO 639-1 코드 (예: "ko", "ja", "en")
|
||||
"""
|
||||
lang_map = {
|
||||
'korean': 'ko',
|
||||
'japanese': 'ja',
|
||||
'english': 'en',
|
||||
'chinese': 'zh',
|
||||
'french': 'fr',
|
||||
'german': 'de',
|
||||
'spanish': 'es',
|
||||
'italian': 'it',
|
||||
'russian': 'ru',
|
||||
'arabic': 'ar',
|
||||
'portuguese': 'pt',
|
||||
'korea': 'ko', # 별칭
|
||||
'japan': 'ja', # 별칭
|
||||
}
|
||||
|
||||
if lang_name:
|
||||
return lang_map.get(lang_name.lower(), lang_name)
|
||||
return ""
|
||||
|
||||
#
|
||||
def ProcessMetadataToCBZ(strArcPath: str, bPrint: bool = False,
|
||||
volume: str = "-1", number: str = "", count: int = -1,
|
||||
info_title: str = None, info_writer: str = None,
|
||||
info_series: str = None, tags: list[str] = None) -> bool:
|
||||
"""
|
||||
CBZ 파일의 .metadata를 읽어 ComicInfo.xml을 생성하고 CBZ에 추가합니다.
|
||||
info_title/info_writer/info_series에 s/pattern/replacement/ 문법을 사용하면
|
||||
.metadata의 원본 값에 정규식 치환을 적용합니다.
|
||||
|
||||
Args:
|
||||
strArcPath: CBZ 파일 경로
|
||||
bPrint: ComicInfo.xml 내용을 출력할지 여부
|
||||
volume: 권/화 묶음 번호 (예: 3)
|
||||
number: 개별 권/화 번호 (예: 1)
|
||||
count: 시리즈 총 권수/화수
|
||||
info_title: 제목 오버라이드 (또는 s/// 정규식)
|
||||
info_writer: 작가 오버라이드 (또는 s/// 정규식)
|
||||
info_series: 시리즈 오버라이드 (또는 s/// 정규식)
|
||||
tags: 태그 리스트 오버라이드
|
||||
|
||||
Returns:
|
||||
성공 여부
|
||||
"""
|
||||
try:
|
||||
util.DbgOut(f".metadata 처리 중: {strArcPath}", True)
|
||||
|
||||
xml_content = MetadataToComicInfoXML(strArcPath, volume=volume, number=number, count=count)
|
||||
|
||||
if not xml_content:
|
||||
util.DbgOut(f"ComicInfo.xml 생성 실패: {strArcPath}", True)
|
||||
return False
|
||||
|
||||
# XML 파싱해서 추가 필드 삽입
|
||||
try:
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
# 현재 값 읽기 (regex 치환용)
|
||||
cur_title = root.findtext('Title') if root.find('Title') is not None else ""
|
||||
cur_writer = root.findtext('Writer') if root.find('Writer') is not None else ""
|
||||
cur_series = root.findtext('Series') if root.find('Series') is not None else ""
|
||||
|
||||
# Regex 치환 적용
|
||||
info_title = ResolveRegexTransform(info_title, cur_title)
|
||||
info_writer = ResolveRegexTransform(info_writer, cur_writer)
|
||||
info_series = ResolveRegexTransform(info_series, cur_series)
|
||||
|
||||
# Title 오버라이드
|
||||
if info_title is not None and info_title != "":
|
||||
title_elem = root.find('Title')
|
||||
if title_elem is None:
|
||||
title_elem = ET.SubElement(root, 'Title')
|
||||
title_elem.text = info_title
|
||||
|
||||
# Writer 오버라이드
|
||||
if info_writer is not None and info_writer != "":
|
||||
writer_elem = root.find('Writer')
|
||||
if writer_elem is None:
|
||||
writer_elem = ET.SubElement(root, 'Writer')
|
||||
writer_elem.text = info_writer
|
||||
|
||||
# Series 오버라이드
|
||||
if info_series is not None and info_series != "":
|
||||
series_elem = root.find('Series')
|
||||
if series_elem is None:
|
||||
series_elem = ET.SubElement(root, 'Series')
|
||||
series_elem.text = info_series
|
||||
|
||||
# Tags 오버라이드
|
||||
if tags is not None and len(tags) > 0:
|
||||
tags_elem = root.find('Tags')
|
||||
if tags_elem is not None:
|
||||
root.remove(tags_elem)
|
||||
tags_elem = ET.SubElement(root, 'Tags')
|
||||
tags_elem.text = ','.join(tags)
|
||||
|
||||
# Volume 추가
|
||||
if volume is not None and volume != "-1" and volume != "":
|
||||
volume_elem = root.find('Volume')
|
||||
if volume_elem is None:
|
||||
volume_elem = ET.SubElement(root, 'Volume')
|
||||
volume_elem.text = str(volume)
|
||||
|
||||
# Number 추가
|
||||
if number is not None and number != "" and number != "-1":
|
||||
number_elem = root.find('Number')
|
||||
if number_elem is None:
|
||||
number_elem = ET.SubElement(root, 'Number')
|
||||
number_elem.text = str(number)
|
||||
|
||||
# Count 추가
|
||||
if count is not None and count != -1:
|
||||
count_elem = root.find('Count')
|
||||
if count_elem is None:
|
||||
count_elem = ET.SubElement(root, 'Count')
|
||||
count_elem.text = str(count)
|
||||
|
||||
# XML을 문자열로 변환
|
||||
tree = ET.ElementTree(root)
|
||||
output = BytesIO()
|
||||
tree.write(output, encoding='utf-8', xml_declaration=True)
|
||||
xml_content = output.getvalue().decode('utf-8')
|
||||
except Exception as e:
|
||||
util.DbgOut(f"XML 수정 중 오류: {e}", True)
|
||||
|
||||
# 출력 옵션이 켜져 있으면 내용 출력
|
||||
if True == bPrint:
|
||||
print(xml_content)
|
||||
|
||||
# XML을 바이트로 변환하여 CBZ에 추가
|
||||
xml_bytes = xml_content.encode('utf-8')
|
||||
|
||||
if True == util.AddFileToZip(strArcPath, "ComicInfo.xml", xml_bytes):
|
||||
util.DbgOut(f"ComicInfo.xml이 CBZ에 추가됨: {strArcPath}", True)
|
||||
return True
|
||||
else:
|
||||
util.DbgOut(f"CBZ에 ComicInfo.xml 추가 실패: {strArcPath}", True)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
util.DbgOut(f".metadata 처리 중 오류 발생: {e}", True)
|
||||
return False
|
||||
|
||||
#
|
||||
def MetadataToComicInfoXML(strArcPath: str, volume: str = "", number: str = "", count: int = -1) -> str:
|
||||
try:
|
||||
listContents = util.GetZipContentList(strArcPath)
|
||||
metadata_file = None
|
||||
|
||||
for item in listContents:
|
||||
if item.lower() == ".metadata":
|
||||
metadata_file = item
|
||||
break
|
||||
|
||||
if not metadata_file:
|
||||
util.DbgOut(f".metadata 파일을 찾을 수 없습니다: {strArcPath}", True)
|
||||
return ""
|
||||
|
||||
metadata_bytes = util.GetZippedFileByte(strArcPath, metadata_file)
|
||||
if not metadata_bytes:
|
||||
util.DbgOut(f".metadata 파일을 읽을 수 없습니다: {strArcPath}", True)
|
||||
return ""
|
||||
|
||||
metadata_json = json.loads(metadata_bytes.decode('utf-8'))
|
||||
|
||||
info_title = ""
|
||||
info_writer = ""
|
||||
info_series = ""
|
||||
info_language = ""
|
||||
tags = []
|
||||
characters = ""
|
||||
teams = ""
|
||||
pages = []
|
||||
|
||||
gallery_block = metadata_json.get('galleryBlock', {})
|
||||
info_title = gallery_block.get('title', '')
|
||||
|
||||
artists = gallery_block.get('artists', [])
|
||||
if artists:
|
||||
info_writer = ', '.join(artists)
|
||||
|
||||
series = gallery_block.get('series', [])
|
||||
if series:
|
||||
info_series = series[0]
|
||||
|
||||
lang_name = gallery_block.get('language', '')
|
||||
info_language = ConvertLanguageToISO(lang_name)
|
||||
|
||||
related_tags = gallery_block.get('relatedTags', [])
|
||||
if related_tags:
|
||||
tags = related_tags
|
||||
|
||||
groups_block = gallery_block.get('groups', [])
|
||||
if groups_block and isinstance(groups_block, list):
|
||||
teams = ','.join([str(g) for g in groups_block if g])
|
||||
|
||||
gallery_info = metadata_json.get('galleryInfo', {})
|
||||
|
||||
chars_info = gallery_info.get('characters', [])
|
||||
if chars_info and isinstance(chars_info, list):
|
||||
char_names = [c.get('character', '') for c in chars_info if isinstance(c, dict)]
|
||||
characters = ','.join([c for c in char_names if c])
|
||||
|
||||
groups_info = gallery_info.get('groups', [])
|
||||
if groups_info and isinstance(groups_info, list):
|
||||
group_names = [g.get('group', '') for g in groups_info if isinstance(g, dict)]
|
||||
teams = ','.join([g for g in group_names if g])
|
||||
|
||||
files = gallery_info.get('files', [])
|
||||
for image_index, file_info in enumerate(files):
|
||||
page_data = {
|
||||
'Image': image_index
|
||||
}
|
||||
pages.append(page_data)
|
||||
|
||||
return CreateComicInfoXML(info_title, info_writer, info_series, tags, pages, info_language, characters, teams, volume, number, count)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
util.DbgOut(f"JSON 파싱 오류: {e}", True)
|
||||
return ""
|
||||
except Exception as e:
|
||||
util.DbgOut(f".metadata 변환 중 오류 발생: {e}", True)
|
||||
return ""
|
||||
|
||||
#
|
||||
def CreateComicInfoXML(info_title, info_writer, info_series, tags: list, pages: list, language: str = "", characters: str = "", teams: str = "", volume: str = "", number: str = "", count: int = -1) -> str:
|
||||
"""
|
||||
새로운 ComicInfo.xml을 생성합니다.
|
||||
|
||||
Args:
|
||||
info_title: 제목
|
||||
info_writer: 작가
|
||||
info_series: 시리즈
|
||||
tags: 태그 리스트
|
||||
pages: 페이지 정보 리스트 (dict 또는 객체)
|
||||
language: 언어 정보 (LanguageISO)
|
||||
|
||||
Returns:
|
||||
ComicInfo.xml의 XML 문자열
|
||||
"""
|
||||
try:
|
||||
# 루트 엘리먼트 생성
|
||||
root = ET.Element('ComicInfo')
|
||||
|
||||
# Title 추가
|
||||
if info_title is not None and info_title != "":
|
||||
title_elem = ET.SubElement(root, 'Title')
|
||||
title_elem.text = info_title
|
||||
|
||||
# Series 추가
|
||||
if info_series is not None and info_series != "":
|
||||
series_elem = ET.SubElement(root, 'Series')
|
||||
series_elem.text = info_series
|
||||
|
||||
# Writer 추가
|
||||
if info_writer is not None and info_writer != "":
|
||||
writer_elem = ET.SubElement(root, 'Writer')
|
||||
writer_elem.text = info_writer
|
||||
|
||||
# Tags 추가 (쉼표로 구분)
|
||||
if tags is not None and len(tags) > 0:
|
||||
# 빈 문자열 제거
|
||||
filtered_tags = [tag.strip() for tag in tags if tag and tag.strip()]
|
||||
if filtered_tags:
|
||||
tags_elem = ET.SubElement(root, 'Tags')
|
||||
tags_elem.text = ','.join(filtered_tags)
|
||||
|
||||
# Characters 추가
|
||||
if characters is not None and characters != "":
|
||||
char_elem = ET.SubElement(root, 'Characters')
|
||||
char_elem.text = characters
|
||||
|
||||
# Teams 추가 (groups -> Teams)
|
||||
if teams is not None and teams != "":
|
||||
teams_elem = ET.SubElement(root, 'Teams')
|
||||
teams_elem.text = teams
|
||||
|
||||
# LanguageISO 추가
|
||||
if language is not None and language != "":
|
||||
language_elem = ET.SubElement(root, 'LanguageISO')
|
||||
language_elem.text = language
|
||||
|
||||
# Manga 항목 추가 (만화 유형 - Yes/No/YesAndRightToLeft)
|
||||
manga_elem = ET.SubElement(root, 'Manga')
|
||||
manga_elem.text = "Yes"
|
||||
|
||||
# Volume 추가 (기본값 -1)
|
||||
if volume is not None and volume != "":
|
||||
volume_elem = ET.SubElement(root, 'Volume')
|
||||
volume_elem.text = str(volume)
|
||||
|
||||
# Number 추가 (개별 권/화 번호)
|
||||
if number is not None and number != "" and number != "-1":
|
||||
number_elem = ET.SubElement(root, 'Number')
|
||||
number_elem.text = str(number)
|
||||
|
||||
# Count 추가 (기본값 -1)
|
||||
if count is not None:
|
||||
count_elem = ET.SubElement(root, 'Count')
|
||||
count_elem.text = str(count)
|
||||
|
||||
# Pages 추가
|
||||
if pages is not None and len(pages) > 0:
|
||||
pages_elem = ET.SubElement(root, 'Pages')
|
||||
for page in pages:
|
||||
page_elem = ET.SubElement(pages_elem, 'Page')
|
||||
|
||||
# Type 속성 추가 (고정값: Story)
|
||||
page_elem.set('Type', 'Story')
|
||||
|
||||
# Image 속성 추가
|
||||
if isinstance(page, dict):
|
||||
if 'Image' in page and page['Image'] is not None:
|
||||
page_elem.set('Image', str(page['Image']))
|
||||
else:
|
||||
# page가 객체인 경우, Image 속성만 추출
|
||||
if hasattr(page, 'Image'):
|
||||
page_elem.set('Image', str(page.Image))
|
||||
|
||||
# XML을 문자열로 변환
|
||||
tree = ET.ElementTree(root)
|
||||
output = BytesIO()
|
||||
tree.write(output, encoding='utf-8', xml_declaration=True)
|
||||
|
||||
return output.getvalue().decode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
util.DbgOut(f"ComicInfo.xml 생성 중 오류 발생: {e}", True)
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
# For Main Loop
|
||||
if __name__ == '__main__':
|
||||
argc = len(sys.argv)
|
||||
argv = sys.argv
|
||||
main(argc, argv)
|
||||
|
||||
#
|
||||
def printHowto():
|
||||
print("Usage: python main.py [options]")
|
||||
print("Options:")
|
||||
print(" -h, --help Show help message.")
|
||||
print(" -v, --version Show version.")
|
||||
Reference in New Issue
Block a user