v0.5.1: Mobile UX improvements - Custom notify styling, nav margin fixes, search button optimization

This commit is contained in:
2026-01-02 15:37:55 +09:00
parent 4e9203ed00
commit c662d2dadc
16 changed files with 2215 additions and 592 deletions

View File

@@ -57,7 +57,7 @@ from .lib.crawler import Crawler
# 패키지
# from .plugin import P
from .lib.util import Util, yommi_timeit
from .lib.util import Util as AniUtil, yommi_timeit
from typing import Awaitable, TypeVar
T = TypeVar("T")
@@ -78,6 +78,7 @@ class LogicAniLife(AnimeModuleBase):
"anilife_finished_insert": "[완결]",
"anilife_max_ffmpeg_process_count": "1",
"anilife_download_method": "ffmpeg", # ffmpeg or ytdlp
"anilife_download_threads": "16", # yt-dlp/aria2c 병렬 쓰레드 수
"anilife_order_desc": "False",
"anilife_auto_start": "False",
"anilife_interval": "* 5 * * *",
@@ -164,9 +165,42 @@ class LogicAniLife(AnimeModuleBase):
def __init__(self, P):
super(LogicAniLife, self).__init__(P, setup_default=self.db_default, name=name, first_menu='setting', scheduler_desc="애니라이프 자동 다운로드")
self.queue = None
self.web_list_model = ModelAniLifeItem
self.OS_PLATFORM = platform.system()
default_route_socketio_module(self, attach="/search")
def process_command(self, command, arg1, arg2, arg3, req):
try:
if command == "list":
ret = self.queue.get_entity_list() if self.queue else []
return jsonify(ret)
elif command == "stop":
entity_id = int(arg1) if arg1 else -1
result = self.queue.command("cancel", entity_id) if self.queue else {"ret": "error"}
return jsonify(result)
elif command == "remove":
entity_id = int(arg1) if arg1 else -1
result = self.queue.command("remove", entity_id) if self.queue else {"ret": "error"}
return jsonify(result)
elif command in ["reset", "delete_completed"]:
result = self.queue.command(command, 0) if self.queue else {"ret": "error"}
return jsonify(result)
elif command == "merge_subtitle":
# AniUtil already imported at module level
db_id = int(arg1)
db_item = ModelAniLifeItem.get_by_id(db_id)
if db_item and db_item.status == 'completed':
import threading
threading.Thread(target=AniUtil.merge_subtitle, args=(self.P, db_item)).start()
return jsonify({"ret": "success", "log": "자막 합칩을 시작합니다."})
return jsonify({"ret": "fail", "log": "파일을 찾을 수 없거나 완료된 상태가 아닙니다."})
return jsonify({"ret": "fail", "log": f"Unknown command: {command}"})
except Exception as e:
self.P.logger.error(f"process_command Error: {e}")
self.P.logger.error(traceback.format_exc())
return jsonify({'ret': 'fail', 'log': str(e)})
# @staticmethod
def get_html(
self,
@@ -578,11 +612,18 @@ class LogicAniLife(AnimeModuleBase):
socketio.emit(
"notify", notify, namespace="/framework", broadcast=True
)
thread = threading.Thread(target=func, args=())
thread.daemon = True
thread.start()
return jsonify("")
elif sub == "proxy_image":
image_url = request.args.get("url") or request.args.get("image_url")
return self.proxy_image(image_url)
elif sub == "entity_list":
if self.queue is not None:
return jsonify(self.queue.get_entity_list())
else:
return jsonify([])
elif sub == "web_list":
return jsonify(ModelAniLifeItem.web_list(request))
elif sub == "db_remove":
@@ -657,48 +698,14 @@ class LogicAniLife(AnimeModuleBase):
except Exception as e:
logger.error(f"browse_dir error: {e}")
return jsonify({"ret": "error", "error": str(e)}), 500
return jsonify({"ret": "fail", "log": f"Unknown sub: {sub}"})
except Exception as e:
P.logger.error("Exception:%s", e)
P.logger.error("AniLife process_ajax Exception:%s", e)
P.logger.error(traceback.format_exc())
return jsonify({"ret": "exception", "log": str(e)})
def process_command(self, command, arg1, arg2, arg3, req):
ret = {"ret": "success"}
logger.debug("queue_list")
if command == "queue_list":
logger.debug(
f"self.queue.get_entity_list():: {self.queue.get_entity_list()}"
)
ret = [x for x in self.queue.get_entity_list()]
return ret
elif command == "download_program":
_pass = arg2
db_item = ModelOhli24Program.get(arg1)
if _pass == "false" and db_item != None:
ret["ret"] = "warning"
ret["msg"] = "이미 DB에 있는 항목 입니다."
elif (
_pass == "true"
and db_item != None
and ModelOhli24Program.get_by_id_in_queue(db_item.id) != None
):
ret["ret"] = "warning"
ret["msg"] = "이미 큐에 있는 항목 입니다."
else:
if db_item == None:
db_item = ModelOhli24Program(arg1, self.get_episode(arg1))
db_item.save()
db_item.init_for_queue()
self.download_queue.put(db_item)
ret["msg"] = "다운로드를 추가 하였습니다."
elif command == "list":
# Anilife 큐의 entity_list 반환 (이전: SupportFfmpeg.get_list() - 잘못된 소스)
ret = []
for entity in self.queue.entity_list:
ret.append(entity.as_dict())
return jsonify(ret)
@staticmethod
def add_whitelist(*args):
@@ -765,16 +772,50 @@ class LogicAniLife(AnimeModuleBase):
self.queue = FfmpegQueue(
P, P.ModelSetting.get_int("anilife_max_ffmpeg_process_count"), name, self
)
self.queue.queue_start()
# 데이터 마이그레이션/동기화: 파일명이 비어있는 항목들 처리
from framework import app
with app.app_context():
try:
items = ModelAniLifeItem.get_list_uncompleted()
for item in items:
if not item.filename or item.filename == item.title:
# 임시로 Entity를 만들어 파일명 생성 로직 활용
tmp_info = item.anilife_info if item.anilife_info else {}
# dict가 아닐 경우 처리 (문자열 등)
if isinstance(tmp_info, str):
try: tmp_info = json.loads(tmp_info)
except: tmp_info = {}
tmp_entity = AniLifeQueueEntity(P, self, tmp_info)
if tmp_entity.filename:
item.filename = tmp_entity.filename
item.save()
logger.info(f"Synced filename for item {item.id}: {item.filename}")
except Exception as e:
logger.error(f"Data sync error: {e}")
logger.error(traceback.format_exc())
self.current_data = None
self.queue.queue_start()
# Camoufox 미리 준비 (백그라운드에서 설치 및 바이너리 다운로드)
threading.Thread(target=self.ensure_camoufox_installed, daemon=True).start()
def db_delete(self, day):
try:
# 전체 삭제 (일수 기준 또는 전체)
return ModelAniLifeItem.delete_all()
except Exception as e:
logger.error(f"Exception: {str(e)}")
logger.error(traceback.format_exc())
return False
def scheduler_function(self):
logger.debug(f"ohli24 scheduler_function::=========================")
content_code_list = P.ModelSetting.get_list("ohli24_auto_code_list", "|")
content_code_list = P.ModelSetting.get_list("anilife_auto_code_list", "|")
url = f'{P.ModelSetting.get("anilife_url")}/dailyani'
if "all" in content_code_list:
ret_data = LogicAniLife.get_auto_anime_info(self, url=url)
@@ -1158,9 +1199,39 @@ class LogicAniLife(AnimeModuleBase):
return data
except Exception as e:
P.logger.error(f"Exception: {str(e)}")
P.logger.error(f"AniLife process_ajax Error: {str(e)}")
P.logger.error(traceback.format_exc())
return {"ret": "exception", "log": str(e)}
return jsonify({"ret": "exception", "log": str(e)})
def proxy_image(self, image_url):
try:
if not image_url or image_url == "None":
return ""
import requests
headers = {
'Referer': 'https://anilife.live/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
res = requests.get(image_url, headers=headers, stream=True, timeout=10)
from flask import Response
return Response(res.content, mimetype=res.headers.get('content-type', 'image/jpeg'))
except Exception as e:
P.logger.error(f"AniLife proxy_image error: {e}")
return ""
def vtt_proxy(self, vtt_url):
try:
import requests
headers = {
'Referer': 'https://anilife.live/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
res = requests.get(vtt_url, headers=headers, timeout=10)
from flask import Response
return Response(res.text, mimetype='text/vtt')
except Exception as e:
P.logger.error(f"AniLife vtt_proxy error: {e}")
return ""
#########################################################
def add(self, episode_info):
@@ -1210,8 +1281,27 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
self.content_title = None
self.srt_url = None
self.headers = None
# [Lazy Extraction] __init__에서는 무거운 분석을 하지 않습니다.
# self.make_episode_info()
self.filename = info.get("title")
self.epi_queue = info.get("ep_num")
self.content_title = info.get("title")
def get_downloader(self, video_url, output_file, callback=None, callback_function=None):
from .lib.downloader_factory import DownloaderFactory
# Anilife는 설정이 따로 없으면 기본 ytdlp 사용하거나 ffmpeg
method = self.P.ModelSetting.get("anilife_download_method") or "ffmpeg"
threads = self.P.ModelSetting.get_int("anilife_download_threads") or 16
logger.info(f"AniLife get_downloader using method: {method}, threads: {threads}")
return DownloaderFactory.get_downloader(
method=method,
video_url=video_url,
output_file=output_file,
headers=self.headers,
callback=callback,
callback_id="anilife",
threads=threads,
callback_function=callback_function
)
def refresh_status(self):
self.module_logic.socketio_callback("status", self.as_dict())
@@ -1223,16 +1313,29 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
tmp["vtt"] = self.vtt
tmp["season"] = self.season
tmp["content_title"] = self.content_title
# 큐 리스트에서 '에피소드 제목'으로 명확히 인지되도록 함
tmp["episode_title"] = self.info.get("title")
tmp["anilife_info"] = self.info
tmp["epi_queue"] = self.epi_queue
tmp["filename"] = self.filename
return tmp
def donwload_completed(self):
db_entity = ModelAniLifeItem.get_by_anilife_id(self.info["_id"])
if db_entity is not None:
db_entity.status = "completed"
db_entity.complated_time = datetime.now()
db_entity.completed_time = datetime.now()
# 메타데이터 동기화
db_entity.filename = self.filename
db_entity.save_fullpath = self.save_fullpath
db_entity.filesize = self.filesize
db_entity.duration = self.duration
db_entity.quality = self.quality
db_entity.save()
# Discord 알림 (이미 메인에서 처리될 수도 있으나 명시적으로 필요한 경우)
# if self.P.ModelSetting.get_bool('anilife_discord_notification'):
# ...
def prepare_extra(self):
"""
@@ -1305,7 +1408,11 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
def log_stderr(pipe):
for line in iter(pipe.readline, ''):
if line.strip():
logger.info(f"[Camoufox] {line.strip()}")
# tqdm 진행바나 불필요한 로그는 debug 레벨로 출력하여 로그 도배 방지
if '%' in line or '|' in line or 'addon' in line.lower():
logger.debug(f"[Camoufox-Progress] {line.strip()}")
else:
logger.info(f"[Camoufox] {line.strip()}")
stderr_thread = threading.Thread(target=log_stderr, args=(process.stderr,))
stderr_thread.start()
@@ -1314,7 +1421,13 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
for line in iter(process.stdout.readline, ''):
stdout_data.append(line)
process.wait(timeout=120)
try:
process.wait(timeout=120)
except subprocess.TimeoutExpired:
logger.error("Camoufox subprocess timed out (120s)")
process.kill()
return
stderr_thread.join(timeout=5)
stdout_full = "".join(stdout_data)
@@ -1468,26 +1581,25 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
self.epi_queue = epi_no
self.filename = Util.change_text_for_use_filename(ret)
self.filename = AniUtil.change_text_for_use_filename(ret)
logger.info(f"Filename: {self.filename}")
# anilife 전용 다운로드 경로 설정 (ohli24_download_path 대신 anilife_download_path 사용)
# anilife 전용 다운로드 경로 설정
self.savepath = P.ModelSetting.get("anilife_download_path")
if not self.savepath:
self.savepath = P.ModelSetting.get("ohli24_download_path")
logger.info(f"Savepath: {self.savepath}")
if P.ModelSetting.get_bool("ohli24_auto_make_folder"):
if P.ModelSetting.get_bool("anilife_auto_make_folder"):
if self.info.get("day", "").find("완결") != -1:
folder_name = "%s %s" % (
P.ModelSetting.get("ohli24_finished_insert"),
P.ModelSetting.get("anilife_finished_insert"),
self.content_title,
)
else:
folder_name = self.content_title
folder_name = Util.change_text_for_use_filename(folder_name.strip())
folder_name = AniUtil.change_text_for_use_filename(folder_name.strip())
self.savepath = os.path.join(self.savepath, folder_name)
if P.ModelSetting.get_bool("ohli24_auto_make_season_folder"):
if P.ModelSetting.get_bool("anilife_auto_make_season_folder"):
self.savepath = os.path.join(
self.savepath, "Season %s" % int(self.season)
)
@@ -1547,12 +1659,17 @@ class ModelAniLifeItem(db.Model):
def as_dict(self):
ret = {x.name: getattr(self, x.name) for x in self.__table__.columns}
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S")
ret["created_time"] = self.created_time.strftime("%Y-%m-%d %H:%M:%S") if self.created_time is not None else None
ret["completed_time"] = (
self.completed_time.strftime("%Y-%m-%d %H:%M:%S")
if self.completed_time is not None
else None
)
# 템플릿 호환용 (anilife_list.html)
ret["image_link"] = self.thumbnail
ret["ep_num"] = self.episode_no
# content_title이 없으면 제목(시리즈명)으로 활용
ret["content_title"] = self.anilife_info.get("content_title") if self.anilife_info else self.title
return ret
def save(self):
@@ -1569,9 +1686,33 @@ class ModelAniLifeItem(db.Model):
@classmethod
def delete_by_id(cls, idx):
db.session.query(cls).filter_by(id=idx).delete()
db.session.commit()
return True
try:
logger.debug(f"delete_by_id: {idx} (type: {type(idx)})")
if isinstance(idx, str) and ',' in idx:
id_list = [int(x.strip()) for x in idx.split(',') if x.strip()]
logger.debug(f"Batch delete: {id_list}")
count = db.session.query(cls).filter(cls.id.in_(id_list)).delete(synchronize_session='fetch')
logger.debug(f"Deleted count: {count}")
else:
db.session.query(cls).filter_by(id=int(idx)).delete()
logger.debug(f"Single delete: {idx}")
db.session.commit()
return True
except Exception as e:
logger.error(f"Exception: {str(e)}")
logger.error(traceback.format_exc())
return False
@classmethod
def delete_all(cls):
try:
db.session.query(cls).delete()
db.session.commit()
return True
except Exception as e:
logger.error(f"Exception: {str(e)}")
logger.error(traceback.format_exc())
return False
@classmethod
def web_list(cls, req):
@@ -1622,22 +1763,28 @@ class ModelAniLifeItem(db.Model):
@classmethod
def append(cls, q):
# 중복 체크
existing = cls.get_by_anilife_id(q["_id"])
if existing:
logger.debug(f"Item already exists in DB: {q['_id']}")
return existing
item = ModelAniLifeItem()
item.content_code = q["content_code"]
item.season = q["season"]
item.episode_no = q["epi_queue"]
item.episode_no = q.get("epi_queue")
item.title = q["content_title"]
item.episode_title = q["title"]
item.ohli24_va = q["va"]
item.ohli24_vi = q["_vi"]
item.ohli24_id = q["_id"]
item.anilife_va = q.get("va")
item.anilife_vi = q.get("_vi")
item.anilife_id = q["_id"]
item.quality = q["quality"]
item.filepath = q["filepath"]
item.filename = q["filename"]
item.savepath = q["savepath"]
item.video_url = q["url"]
item.vtt_url = q["vtt"]
item.thumbnail = q["thumbnail"]
item.filepath = q.get("filepath")
item.filename = q.get("filename")
item.savepath = q.get("savepath")
item.video_url = q.get("url")
item.vtt_url = q.get("vtt")
item.thumbnail = q.get("thumbnail")
item.status = "wait"
item.ohli24_info = q["anilife_info"]
item.anilife_info = q.get("anilife_info")
item.save()