From 9b3f4f72bd258f2cd71b2f7b5f6be2d997ee16ef Mon Sep 17 00:00:00 2001 From: projectdx Date: Sun, 11 Jan 2026 16:56:35 +0900 Subject: [PATCH] v0.7.2: Linkkf subtitle download, list-request integration, and hot reload stability improvements --- README.md | 16 ++++ info.yaml | 2 +- mod_anilife.py | 2 +- mod_linkkf.py | 66 +++++++++++++++- mod_ohli24.py | 4 +- templates/anime_downloader_linkkf_list.html | 6 ++ .../anime_downloader_linkkf_request.html | 77 ++++++++++--------- 7 files changed, 132 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 290ef33..4beb058 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ ## πŸš€ μ£Όμš” κΈ°λŠ₯ (Key Features) * **닀쀑 μ‚¬μ΄νŠΈ 지원**: Ohli24, Anilife, Linkkf λ“± λ‹€μ–‘ν•œ μ†ŒμŠ€μ—μ„œ μ˜μƒ 검색 및 λ‹€μš΄λ‘œλ“œ. +* **μžλ§‰ μ΅œμ ν™”**: + * **μžλ§‰ μ „μš© λ‹€μš΄λ‘œλ“œ (Linkkf)**: μ˜μƒ 없이 μžλ§‰(VTT)만 λ³„λ„λ‘œ μΆ”μΆœν•˜μ—¬ SRT둜 λ³€ν™˜ ν›„ λ‹€μš΄λ‘œλ“œν•  수 μžˆμŠ΅λ‹ˆλ‹€. + * **μžλ§‰ ν•©μΉ¨ (Muxing)**: λ‹€μš΄λ‘œλ“œλœ μ™ΈλΆ€ μžλ§‰(SRT)을 MP4 μ»¨ν…Œμ΄λ„ˆμ— μžλ™μœΌλ‘œ μ‚½μž…ν•˜μ—¬ λ²”μš©μ„±μ„ λ†’μž…λ‹ˆλ‹€. * **κ°•λ ₯ν•œ 우회 기술 (Anti-Bot Bypass)**: * **TLS Fingerprint λ³€μ‘°**: `curl_cffi`λ₯Ό μ‚¬μš©ν•˜μ—¬ μ‹€μ œ Chrome λΈŒλΌμš°μ €μ²˜λŸΌ μœ„μž₯, Cloudflare 및 각쒅 봇 차단을 무λ ₯ν™”ν•©λ‹ˆλ‹€. * **CDN μžλ™ 감지**: 슀트리밍 μ„œλ²„(CDN)의 도메인이 μˆ˜μ‹œλ‘œ λ³€κ²½λ˜λ”λΌλ„ μžλ™μœΌλ‘œ κ°μ§€ν•˜μ—¬ λŒ€μ‘ν•©λ‹ˆλ‹€. (예: 14B κ°€μ§œ 파일 문제 ν•΄κ²°) @@ -81,6 +84,19 @@ ## πŸ“ λ³€κ²½ 이λ ₯ (Changelog) +### v0.7.2 (2026-01-11) +- **Linkkf μžλ§‰ μ „μš© λ‹€μš΄λ‘œλ“œ 지원**: + - μ—ν”Όμ†Œλ“œ 뢄석 νŽ˜μ΄μ§€μ— **"μžλ§‰λ§Œ λ‹€μš΄λ‘œλ“œ"** λ²„νŠΌ μΆ”κ°€. (VTT μΆ”μΆœ 및 SRT μžλ™ λ³€ν™˜) + - λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œ 처리λ₯Ό 톡해 λŒ€λŸ‰μ˜ μžλ§‰μ„ λŠκΉ€ 없이 λ‹€μš΄λ‘œλ“œ κ°€λŠ₯. +- **Linkkf λͺ©λ‘-뢄석 연동 κ°•ν™”**: + - λͺ©λ‘(`list`) νŽ˜μ΄μ§€ μΉ΄λ“œμ— **"μž‘ν’ˆμ†Œκ°œ"** λ²„νŠΌ μΆ”κ°€ν•˜μ—¬ 원클릭 뢄석 지원. + - URL νŒŒλΌλ―Έν„°(`code`)λ₯Ό ν†΅ν•œ μžλ™ 뢄석 둜직 κ°œμ„ μœΌλ‘œ 연동 νŽΈμ˜μ„± μ¦λŒ€. +- **ν”ŒλŸ¬κ·ΈμΈ ν•« λ¦¬λ‘œλ“œ μ•ˆμ •ν™”**: + - Linkkf, AniLife, Ohli24 λͺ¨λ“  λͺ¨λ“ˆμ˜ DB λͺ¨λΈμ— `extend_existing: True` μ˜΅μ…˜ 적용. + - ν”ŒλŸ¬κ·ΈμΈ μžκ°€ μ—…λ°μ΄νŠΈ μ‹œ DB λͺ¨λΈ μ œμ•½μœΌλ‘œ μΈν•œ λ¦¬λ‘œλ“œ μ‹€νŒ¨ 문제 ν•΄κ²°. +- **UI/UX λ³΄μ•ˆ**: + - 뢄석 νŽ˜μ΄μ§€μ˜ AJAX μš”μ²­ κ²°κ³Ό 핸듀링을 κ°•ν™”ν•˜μ—¬ μ‚¬μš©μžμ—κ²Œ μ •ν™•ν•œ 성곡/μ‹€νŒ¨ μ•Œλ¦Ό 제곡. + ### v0.7.1 (2026-01-11) - **GDM 톡합 버그 μˆ˜μ • (Hotfix)**: - **UI ν•„λ“œ λ§€ν•‘ μˆ˜μ •**: 큐 νŽ˜μ΄μ§€μ—μ„œ GDM μž‘μ—…μ˜ μƒνƒœ, μ§„ν–‰λ₯  등이 'undefined'둜 ν‘œμ‹œλ˜λ˜ ν•„λ“œλͺ… 뢈일치 문제 ν•΄κ²°. diff --git a/info.yaml b/info.yaml index 3d52142..6eddf22 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "μ• λ‹ˆ λ‹€μš΄λ‘œλ”" -version: 0.7.1 +version: 0.7.2 package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/mod_anilife.py b/mod_anilife.py index 256a655..d83da41 100644 --- a/mod_anilife.py +++ b/mod_anilife.py @@ -2228,7 +2228,7 @@ class AniLifeQueueEntity(FfmpegQueueEntity): class ModelAniLifeItem(db.Model): __tablename__ = "{package_name}_anilife_item".format(package_name=P.package_name) - __table_args__ = {"mysql_collate": "utf8_general_ci"} + __table_args__ = {"mysql_collate": "utf8_general_ci", "extend_existing": True} __bind_key__ = P.package_name id = db.Column(db.Integer, primary_key=True) created_time = db.Column(db.DateTime) diff --git a/mod_linkkf.py b/mod_linkkf.py index 186a74c..7d3ea41 100644 --- a/mod_linkkf.py +++ b/mod_linkkf.py @@ -196,7 +196,6 @@ class LogicLinkkf(AnimeModuleBase): logger.error(traceback.format_exc()) ret["ret"] = "error" ret["log"] = str(e) - return jsonify(ret) elif sub == "add_queue_checked_list": # μ„ νƒλœ μ—ν”Όμ†Œλ“œ 일괄 μΆ”κ°€ (λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œλ‘œ 처리) import threading @@ -252,6 +251,69 @@ class LogicLinkkf(AnimeModuleBase): ret["ret"] = "error" ret["log"] = str(e) return jsonify(ret) + elif sub == "add_sub_queue_checked_list": + # μ„ νƒλœ μ—ν”Όμ†Œλ“œ μžλ§‰λ§Œ 일괄 λ‹€μš΄λ‘œλ“œ (λ°±κ·ΈλΌμš΄λ“œ μŠ€λ ˆλ“œλ‘œ 처리) + import threading + from flask import current_app + + logger.info("========= add_sub_queue_checked_list START =========") + ret = {"ret": "success", "message": "λ°±κ·ΈλΌμš΄λ“œμ—μ„œ μžλ§‰ λ‹€μš΄λ‘œλ“œ 쀑..."} + try: + form_data = request.form.get("data") + if not form_data: + ret["ret"] = "error" + ret["log"] = "No data received" + return jsonify(ret) + + episode_list = json.loads(form_data) + logger.info(f"Received {len(episode_list)} episodes to download subtitles in background") + + # Flask app μ°Έμ‘° μ €μž₯ + app = current_app._get_current_object() + + def download_subtitles_background(flask_app, episode_list): + added = 0 + skipped = 0 + with flask_app.app_context(): + for episode_info in episode_list: + try: + # LinkkfQueueEntityλ₯Ό μ‚¬μš©ν•˜μ—¬ μžλ§‰ URL μΆ”μΆœ (prepare_extra ν™œμš©) + entity = LinkkfQueueEntity(P, self, episode_info) + entity.prepare_extra() + + if entity.vtt: + # μžλ§‰ λ‹€μš΄λ‘œλ“œ 및 λ³€ν™˜ + # entity.filepathλŠ” prepare_extraμ—μ„œ 섀정됨 (κΈ°λ³Έ μ €μž₯ 경둜 + 파일λͺ…) + res = Util.download_subtitle(entity.vtt, entity.filepath, headers=entity.headers) + if res: + added += 1 + logger.debug(f"Downloaded subtitle for {episode_info.get('title')}") + else: + skipped += 1 + logger.info(f"Failed to download subtitle for {episode_info.get('title')}") + else: + skipped += 1 + logger.info(f"No subtitle found for {episode_info.get('title')}") + except Exception as e: + logger.error(f"Error in download_subtitles_background for one episode: {e}") + skipped += 1 + + logger.info(f"add_sub_queue_checked_list completed: downloaded={added}, skipped={skipped}") + + thread = threading.Thread( + target=download_subtitles_background, + args=(app, episode_list) + ) + thread.daemon = True + thread.start() + + ret["count"] = len(episode_list) + except Exception as e: + logger.error(f"add_sub_queue_checked_list error: {e}") + logger.error(traceback.format_exc()) + ret["ret"] = "error" + ret["log"] = str(e) + return jsonify(ret) elif sub == "web_list": ret = ModelLinkkfItem.web_list(req) return jsonify(ret) @@ -2526,7 +2588,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity): class ModelLinkkfItem(db.Model): __tablename__ = "{package_name}_linkkf_item".format(package_name=P.package_name) - __table_args__ = {"mysql_collate": "utf8_general_ci"} + __table_args__ = {"mysql_collate": "utf8_general_ci", "extend_existing": True} __bind_key__ = P.package_name id = db.Column(db.Integer, primary_key=True) created_time = db.Column(db.DateTime) diff --git a/mod_ohli24.py b/mod_ohli24.py index 4cdd63b..f82b21a 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -3447,7 +3447,7 @@ class Ohli24QueueEntity(AnimeQueueEntity): class ModelOhli24Item(ModelBase): P = P __tablename__ = "{package_name}_ohli24_item".format(package_name=P.package_name) - __table_args__ = {"mysql_collate": "utf8_general_ci"} + __table_args__ = {"mysql_collate": "utf8_general_ci", "extend_existing": True} __bind_key__ = P.package_name id = db.Column(db.Integer, primary_key=True) created_time = db.Column(db.DateTime) @@ -3603,7 +3603,7 @@ class ModelOhli24Item(ModelBase): class ModelOhli24Program(ModelBase): P = P __tablename__ = f"{P.package_name}_{name}_program" - __table_args__ = {"mysql_collate": "utf8_general_ci"} + __table_args__ = {"mysql_collate": "utf8_general_ci", "extend_existing": True} __bind_key__ = P.package_name id = db.Column(db.Integer, primary_key=True) diff --git a/templates/anime_downloader_linkkf_list.html b/templates/anime_downloader_linkkf_list.html index 2974465..43df903 100644 --- a/templates/anime_downloader_linkkf_list.html +++ b/templates/anime_downloader_linkkf_list.html @@ -768,6 +768,12 @@ $(document).ready(function(){ str += ''; str += ''; } + + // [μž‘ν’ˆμ†Œκ°œ] λ²„νŠΌ μΆ”κ°€ - JSON λ²„νŠΌ μ™Όμͺ½μ— 배치 + if (item.content_code) { + str += ''; + } + str += ''; str += ''; str += ''; diff --git a/templates/anime_downloader_linkkf_request.html b/templates/anime_downloader_linkkf_request.html index 8df3eeb..2c408e8 100644 --- a/templates/anime_downloader_linkkf_request.html +++ b/templates/anime_downloader_linkkf_request.html @@ -195,45 +195,21 @@ } $(function () { - // console.log(params.wr_id) - // console.log(findGetParameter('wr_id')) - // console.log(params.code) - if (params.code === '') { - - } else { - document.getElementById("code").value = params.code - // {#document.getElementById("analysis_btn").click();#} + // URL νŒŒλΌλ―Έν„° 처리 (code) + const urlCode = params.code || findGetParameter('code'); + const currentCode = "{{arg['linkkf_current_code']}}"; + + const targetCode = urlCode || currentCode; + + if (targetCode) { + document.getElementById("code").value = targetCode; + // wr_id, bo_table 등이 있으면 같이 전달 + analyze(params.wr_id || findGetParameter('wr_id'), params.bo_table || findGetParameter('bo_table')); } - - if ("{{arg['linkkf_current_code']}}" !== "") { - if (params.code === null) { - // console.log('params.code === null') - document.getElementById("code").value = "{{arg['linkkf_current_code']}}"; - - } else if (params.code === '') { - document.getElementById("code").value = "{{arg['linkkf_current_code']}}"; - } else { - - // console.log('params code exist') - // console.log(params.code) - document.getElementById("code").value = params.code - - analyze(params.wr_id, params.bo_table) - // document.getElementById("analysis_btn").click(); - // $('#analysis_btn').trigger('click') - } - // 값이 곡백이 μ•„λ‹ˆλ©΄ 뢄석 λ²„νŠΌ 계속 λˆ„λ¦„ - // {#document.getElementById("analysis_btn").click();#} - } else { - - } - - }) + }); $(document).ready(function () { - // console.log('wr_id::', params.wr_id) - }); // Enter ν‚€λ‘œ 검색 트리거 @@ -364,6 +340,37 @@ } }); }); + + $("body").on('click', '#down_subtitle_btn', function (e) { + e.preventDefault(); + all = $('input[id^="checkbox_"]'); + let data = []; + let idx; + for (let i in all) { + if (all[i].checked) { + idx = parseInt(all[i].id.split('_')[1]) + data.push(current_data.episode[idx]); + } + } + if (data.length == 0) { + $.notify('μ„ νƒν•˜μ„Έμš”.', {type: 'warning'}); + return; + } + $.ajax({ + url: '/' + package_name + '/ajax/' + sub + '/add_sub_queue_checked_list', + type: "POST", + cache: false, + data: {data: JSON.stringify(data)}, + dataType: "json", + success: function (data) { + if (data.ret == "success") { + $.notify('λ°±κ·ΈλΌμš΄λ“œλ‘œ μžλ§‰ λ‹€μš΄λ‘œλ“œλ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€.', {type: 'success'}); + } else { + $.notify('μžλ§‰ λ‹€μš΄λ‘œλ“œ μš”μ²­ μ‹€νŒ¨: ' + data.log + '', {type: 'warning'}); + } + } + }); + });