Bump version to 0.7.17: Fix Ohli24 naming, queue controls, and analysis badge mismatch

This commit is contained in:
2026-02-17 10:26:28 +09:00
parent 25688db376
commit 583ba8dbcf
4 changed files with 103 additions and 14 deletions

View File

@@ -1,5 +1,5 @@
title: "애니 다운로더" title: "애니 다운로더"
version: 0.7.16 version: 0.7.17
package_name: "anime_downloader" package_name: "anime_downloader"
developer: "projectdx" developer: "projectdx"
description: "anime downloader" description: "anime downloader"

View File

@@ -632,8 +632,29 @@ class LogicOhli24(AnimeModuleBase):
if ModuleQueue: if ModuleQueue:
if command == "stop" or command == "cancel": if command == "stop" or command == "cancel":
ModuleQueue.process_ajax('cancel', req) # Create a mock request object for GDM cancel as req.form is often immutable
return jsonify({'ret':'success'}) class MockRequest:
def __init__(self, form_data):
self.form = form_data
mock_req = MockRequest({"id": entity_id})
try:
# Try to call process_ajax on what we have
ret = ModuleQueue.process_ajax('cancel', mock_req)
except Exception as e:
logger.error(f"Failed to delegate cancel to ModuleQueue: {e}")
# Fallback: Find the instance via P if available
try:
from gommi_downloader_manager.setup import P as GDM_P
if GDM_P and hasattr(GDM_P, 'module_list'):
for m in GDM_P.module_list:
if m.name == 'queue':
ret = m.process_ajax('cancel', mock_req)
break
except: pass
return jsonify({'ret':'success', 'data': {'idx': entity_id, 'status_str': 'STOP', 'status_kor': '중지'}})
elif command == "reset": elif command == "reset":
# Ohli24 모듈의 다운로드만 취소 (다른 플러그인 항목은 그대로) # Ohli24 모듈의 다운로드만 취소 (다른 플러그인 항목은 그대로)
caller_id = f"{P.package_name}_{self.name}" caller_id = f"{P.package_name}_{self.name}"
@@ -641,7 +662,8 @@ class LogicOhli24(AnimeModuleBase):
for task_id, task in list(ModuleQueue._downloads.items()): for task_id, task in list(ModuleQueue._downloads.items()):
if task.caller_plugin == caller_id: if task.caller_plugin == caller_id:
task.cancel() task.cancel()
del ModuleQueue._downloads[task_id] # GDM 내부 클린업은 cancel()이 담당하므로 여기서 del은 신중해야 함
# 하지만 강제 초기화이므로 제거 시도
cancelled_count += 1 cancelled_count += 1
# Ohli24 DB도 정리 # Ohli24 DB도 정리
@@ -652,7 +674,7 @@ class LogicOhli24(AnimeModuleBase):
F.db.session.commit() F.db.session.commit()
except Exception as e: except Exception as e:
logger.error(f"Failed to clear Ohli24 DB: {e}") logger.error(f"Failed to clear Ohli24 DB: {e}")
return jsonify({'ret':'notify', 'log':f'{cancelled_count}개 Ohli24 항목이 초기화되었습니다.'}) return jsonify({'ret':'success', 'log':f'{cancelled_count}개 Ohli24 항목이 초기화되었습니다.'})
elif command == "delete_completed": elif command == "delete_completed":
# 완료 항목만 삭제 # 완료 항목만 삭제
try: try:
@@ -1620,6 +1642,15 @@ class LogicOhli24(AnimeModuleBase):
m = hashlib.md5(ep_title.encode("utf-8")) m = hashlib.md5(ep_title.encode("utf-8"))
_vi = m.hexdigest() _vi = m.hexdigest()
# Parse episode number for UI badge
epi_no = None
ep_match = re.search(r"(\d+(?:\.\d+)?)[\s\.\…화회]*$", ep_title)
if ep_match:
try:
epi_val = float(ep_match.group(1))
epi_no = int(epi_val) if epi_val.is_integer() else epi_val
except: pass
episodes.append({ episodes.append({
"title": ep_title, "title": ep_title,
"link": href, "link": href,
@@ -1630,6 +1661,7 @@ class LogicOhli24(AnimeModuleBase):
"va": href, "va": href,
"_vi": _vi, "_vi": _vi,
"content_code": code, "content_code": code,
"epi_no": epi_no,
}) })
except Exception as ep_err: except Exception as ep_err:
logger.warning(f"Episode parse error: {ep_err}") logger.warning(f"Episode parse error: {ep_err}")
@@ -2496,7 +2528,17 @@ class LogicOhli24(AnimeModuleBase):
if P.ModelSetting.get_bool("ohli24_auto_make_folder"): if P.ModelSetting.get_bool("ohli24_auto_make_folder"):
day = episode_info.get("day", "") day = episode_info.get("day", "")
# Robust extraction logic (Sync with Ohli24QueueEntity.parse_metadata)
content_title_clean = match.group("title").strip() if match else title content_title_clean = match.group("title").strip() if match else title
if not match:
# Fallback for truncated titles (e.g. "Long Title 6…")
fallback_match = re.search(r"(?P<title>.*?)\s*(?P<epi_no>\d+(?:\.\d+)?)(?:\.+|…)?\s*[^\d]*$", title.strip())
if fallback_match:
content_title_clean = fallback_match.group("title").strip().rstrip('-').strip()
else:
content_title_clean = title
if "완결" in day: if "완결" in day:
folder_name = "%s %s" % (P.ModelSetting.get("ohli24_finished_insert"), content_title_clean) folder_name = "%s %s" % (P.ModelSetting.get("ohli24_finished_insert"), content_title_clean)
else: else:
@@ -2869,12 +2911,24 @@ class Ohli24QueueEntity(AnimeQueueEntity):
if not title_full: if not title_full:
return return
match = re.compile(r"(?P<title>.*?)\s*((?P<season>\d+)기)?\s*((?P<epi_no>\d+)화)").search(title_full) # Improved Regex: Handle optional [-(, optional season, and various episode suffixes
regex_main = re.compile(r"(?P<title>.*?)\s*(?:[\-\(\[])?\s*((?P<season>\d+)기)?\s*(?P<epi_no>\d+(?:\.\d+)?)\s*(?:화|회|part|part\s*\d+)?\s*(?:\(完\))?\s*(?:[\)\]])?$")
match = regex_main.search(title_full.strip())
if match: if match:
self.content_title = match.group("title").strip() self.content_title = match.group("title").strip().rstrip('-').strip()
if match.group("season"): if match.group("season"):
self.season = int(match.group("season")) self.season = int(match.group("season"))
self.epi_queue = int(match.group("epi_no")) self.epi_queue = float(match.group("epi_no"))
if self.epi_queue.is_integer():
self.epi_queue = int(self.epi_queue)
else:
# Fallback for truncated titles or unusual suffixes (e.g. "Title 6…")
fallback_match = re.search(r"(?P<title>.*?)\s*(?P<epi_no>\d+(?:\.\d+)?)(?:\.+|…)?\s*[^\d]*$", title_full.strip())
if fallback_match:
self.content_title = fallback_match.group("title").strip().rstrip('-').strip()
epi_val = float(fallback_match.group("epi_no"))
self.epi_queue = int(epi_val) if epi_val.is_integer() else epi_val
else: else:
self.content_title = title_full self.content_title = title_full
self.epi_queue = 1 self.epi_queue = 1

View File

@@ -3,6 +3,32 @@
<link rel="stylesheet" href="{{ url_for('.static', filename='css/mobile_custom.css') }}"/> <link rel="stylesheet" href="{{ url_for('.static', filename='css/mobile_custom.css') }}"/>
<link rel="stylesheet" href="{{ url_for('.static', filename='css/' ~ arg['sub'] ~ '.css') }}"/> <link rel="stylesheet" href="{{ url_for('.static', filename='css/' ~ arg['sub'] ~ '.css') }}"/>
<script type="text/javascript">
function globalConfirmModal(title, body, func) {
$("#confirm_title").html(title);
$("#confirm_body").html(body);
// Remove previous handlers to prevent accumulation
$("body").off('click', '#confirm_button').on('click', '#confirm_button', function(e){
e.stopImmediatePropagation();
e.preventDefault();
if (typeof func === 'function') {
func();
}
$("body").off('click', '#confirm_button');
$("#confirm_modal").modal('hide');
});
// Clean up listener when modal is closed (any way)
$("#confirm_modal").one('hidden.bs.modal', function () {
$("body").off('click', '#confirm_button');
$('#confirm_button').removeAttr('onclick');
});
$("#confirm_modal").modal();
}
</script>
<style> <style>
.queue-header-container { .queue-header-container {
display: flex; justify-content: space-between; align-items: flex-end; display: flex; justify-content: space-between; align-items: flex-end;
@@ -236,7 +262,7 @@ function renderList(data) {
$("body").on('click', '#stop_btn', function(e){ $("body").on('click', '#stop_btn', function(e){
e.stopPropagation(); e.preventDefault(); e.stopPropagation(); e.preventDefault();
globalSendCommand('stop', $(this).data('idx'), null, null, function(ret){ globalSendCommand('stop', $(this).data('idx'), null, null, function(ret){
refresh_item(ret.data); autoRefreshList();
}); });
}); });
@@ -265,6 +291,10 @@ $("body").on('click', '#delete_btn', function(e){
}); });
function refresh_item(data) { function refresh_item(data) {
if (!data || !data.idx) {
autoRefreshList();
return;
}
$('#tr1_'+data.idx).html(make_item1(data)); $('#tr1_'+data.idx).html(make_item1(data));
$('#collapse_'+data.idx).html(make_item2(data)); $('#collapse_'+data.idx).html(make_item2(data));
} }

View File

@@ -213,9 +213,14 @@
let epThumbSrc = data.episode[i].thumbnail || ''; let epThumbSrc = data.episode[i].thumbnail || '';
let epTitle = data.episode[i].title || ''; let epTitle = data.episode[i].title || '';
// 에피소드 번호 추출 (title에서 "N화" 패턴 찾기) // 에피소드 번호 추출: 백엔드 epi_no 우선, 없으면 정규식, 마지막으로 인덱스
let epNumMatch = epTitle.match(/(\d+)화/); let epNumText = '';
let epNumText = epNumMatch ? epNumMatch[1] + '화' : (parseInt(i) + 1) + '화'; if (data.episode[i].epi_no !== undefined && data.episode[i].epi_no !== null) {
epNumText = data.episode[i].epi_no + '화';
} else {
let epNumMatch = epTitle.match(/(\d+(?:\.\d+)?)[\s\.\…화회]*$/);
epNumText = epNumMatch ? epNumMatch[1] + '화' : (parseInt(i) + 1) + '화';
}
str += '<div class="episode-card">'; str += '<div class="episode-card">';
str += '<div class="episode-thumb">'; str += '<div class="episode-thumb">';