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'});
+ }
+ }
+ });
+ });