From 9e25f1f02e2e85e29192aa300812c9a43e507a9b Mon Sep 17 00:00:00 2001 From: projectdx Date: Thu, 1 Jan 2026 00:32:59 +0900 Subject: [PATCH] feat: Refactor download queue UI with real-time updates, add queue management buttons, and streamline socket event handling. --- README.md | 5 + info.yaml | 2 +- lib/ffmpeg_queue_v1.py | 13 +- lib/ytdlp_downloader.py | 33 +- mod_linkkf.py | 302 +++++-- mod_ohli24.py | 11 +- static/js/sjva_global1.js | 2 +- templates/anime_downloader_linkkf_list.html | 773 +++++++++++------ templates/anime_downloader_linkkf_queue.html | 782 ++++++++---------- .../anime_downloader_linkkf_request.html | 167 +++- templates/anime_downloader_linkkf_search.html | 82 +- .../anime_downloader_linkkf_setting.html | 62 +- templates/anime_downloader_ohli24_list.html | 42 +- 13 files changed, 1431 insertions(+), 845 deletions(-) diff --git a/README.md b/README.md index 75af27c..5cecd20 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ ## πŸ“ λ³€κ²½ 이λ ₯ (Changelog) +### v0.3.0 (2025-12-31) +- **VideoJS ν”Œλ ˆμ΄λ¦¬μŠ€νŠΈ**: λΉ„λ””μ˜€ ν”Œλ ˆμ΄μ–΄μ—μ„œ λ‹€μŒ μ—ν”Όμ†Œλ“œ μžλ™ μž¬μƒ +- **ν”Œλ ˆμ΄λ¦¬μŠ€νŠΈ UI**: 이전/λ‹€μŒ λ²„νŠΌ, μ—ν”Όμ†Œλ“œ λͺ©λ‘ ν† κΈ€ +- **μ‹€μ‹œκ°„ κ°±μ‹ **: ν”Œλ ˆμ΄μ–΄ μ—΄λ €μžˆμ„ λ•Œ 10μ΄ˆλ§ˆλ‹€ μƒˆ μ—ν”Όμ†Œλ“œ 감지 및 μ•Œλ¦Ό + ### v0.2.2 (2025-12-31) - **해상도 μžλ™ 감지**: m3u8 master playlistμ—μ„œ 해상도(1080p/720p λ“±)λ₯Ό νŒŒμ‹±ν•˜μ—¬ 파일λͺ…에 반영 - **Discord μ•Œλ¦Ό κ°œμ„ **: 큰 썸넀일 이미지, Discord Blurple 색상, ISO νƒ€μž„μŠ€νƒ¬ν”„ 적용 diff --git a/info.yaml b/info.yaml index 778e5c7..5f9e506 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "μ• λ‹ˆ λ‹€μš΄λ‘œλ”" -version: "0.3.0" +version: "0.3.1" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/lib/ffmpeg_queue_v1.py b/lib/ffmpeg_queue_v1.py index 0891431..0d7d525 100644 --- a/lib/ffmpeg_queue_v1.py +++ b/lib/ffmpeg_queue_v1.py @@ -199,15 +199,20 @@ class FfmpegQueue(object): # except: # logger.debug('program path make fail!!') # 파일 μ‘΄μž¬μ—¬λΆ€ 체크 - print("here...................") - P.logger.info(entity.info) filepath = entity.get_video_filepath() P.logger.debug(f"filepath:: {filepath}") - if os.path.exists(filepath): + + # λ‹€μš΄λ‘œλ“œ 방법 확인 + download_method = P.ModelSetting.get(f"{self.name}_download_method") + + # .ytdl 파일이 μžˆκ±°λ‚˜, ytdlp/aria2c λͺ¨λ“œμΈ 경우 '파일 있음'으둜 κ±΄λ„ˆλ›°μ§€ μ•ŠμŒ (이어받기 ν—ˆμš©) + is_ytdlp = download_method in ['ytdlp', 'aria2c'] + has_ytdl_file = os.path.exists(filepath + ".ytdl") + + if os.path.exists(filepath) and not (is_ytdlp or has_ytdl_file): entity.ffmpeg_status_kor = "파일 있음" entity.ffmpeg_percent = 100 entity.refresh_status() - # plugin.socketio_list_refresh() continue dirname = os.path.dirname(filepath) filename = os.path.basename(filepath) diff --git a/lib/ytdlp_downloader.py b/lib/ytdlp_downloader.py index 219628b..46c5887 100644 --- a/lib/ytdlp_downloader.py +++ b/lib/ytdlp_downloader.py @@ -325,16 +325,30 @@ class YtdlpDownloader: match = prog_re.search(line) if match: try: - self.percent = float(match.group('percent')) + new_percent = float(match.group('percent')) speed_group = match.groupdict().get('speed') + + # 속도가 ν‘œμ‹œλ˜μ§€ μ•ŠλŠ” 경우 (aria2c λ“±)λ₯Ό μœ„ν•΄ μ •κ·œμ‹ 보완 + if not speed_group: + # "[download] 10.5% of ~100.00MiB at 2.45MiB/s" ν˜•νƒœ μž¬ν™•μΈ + at_match = re.search(r'at\s+([\d\.]+\s*\w+/s)', line) + if at_match: + speed_group = at_match.group(1) + if speed_group: self.current_speed = speed_group.strip() + if self.start_time: elapsed = time.time() - self.start_time self.elapsed_time = self.format_time(elapsed) - if self.callback: - logger.info(f"[yt-dlp progress] Calling callback: {int(self.percent)}% speed={self.current_speed}") + + # [μ΅œμ ν™”] μ§„ν–‰λ₯ μ΄ 1% 이상 μ°¨μ΄λ‚˜κ±°λ‚˜, 100%인 κ²½μš°μ—λ§Œ 콜백 호좜 (둜그 λΆ€ν•˜ κ°μ†Œ) + if self.callback and (int(new_percent) > int(self.percent) or new_percent >= 100): + self.percent = new_percent + logger.info(f"[yt-dlp progress] {int(self.percent)}% speed={self.current_speed}") self.callback(percent=int(self.percent), current=int(self.percent), total=100, speed=self.current_speed, elapsed=self.elapsed_time) + else: + self.percent = new_percent except Exception as cb_err: logger.warning(f"Callback error: {cb_err}") break # ν•œ νŒ¨ν„΄μ΄ 맀칭되면 쀑단 @@ -371,3 +385,16 @@ class YtdlpDownloader: def cancel(self): """λ‹€μš΄λ‘œλ“œ μ·¨μ†Œ""" self.cancelled = True + try: + if self.process: + # subprocess μ’…λ₯˜μ— 따라 μ’…λ£Œ 방식 κ²°μ • + if platform.system() == 'Windows': + subprocess.run(['taskkill', '/F', '/T', '/PID', str(self.process.pid)], capture_output=True) + else: + self.process.terminate() + # κ°•μ œ μ’…λ£Œ ν•„μš” μ‹œ + # import signal + # os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + logger.info(f"Ytdlp process {self.process.pid} terminated by cancel()") + except Exception as e: + logger.error(f"Error terminating ytdlp process: {e}") diff --git a/mod_linkkf.py b/mod_linkkf.py index 04f4a41..d4dcca7 100644 --- a/mod_linkkf.py +++ b/mod_linkkf.py @@ -103,9 +103,11 @@ class LogicLinkkf(PluginModuleBase): "linkkf_image_url_prefix_series": "", "linkkf_image_url_prefix_episode": "", "linkkf_discord_notify": "True", - "linkkf_download_method": "ffmpeg", # ffmpeg or ytdlp + "linkkf_download_method": "ffmpeg", # ffmpeg, ytdlp, aria2c + "linkkf_download_threads": "16", # yt-dlp/aria2c 병렬 μ“°λ ˆλ“œ 수 } # default_route_socketio(P, self) + self.web_list_model = ModelLinkkfItem default_route_socketio_module(self, attach="/setting") self.current_data = None @@ -154,7 +156,7 @@ class LogicLinkkf(PluginModuleBase): ) elif sub == "screen_movie_list": try: - logger.debug("request:::> %s", request.form["page"]) + # logger.debug("request:::> %s", request.form["page"]) page = request.form["page"] data = self.get_screen_movie_info(page) dummy_data = {"ret": "success", "data": data} @@ -270,46 +272,128 @@ class LogicLinkkf(PluginModuleBase): ret["log"] = str(e) return jsonify(ret) elif sub == "web_list": - return jsonify({"ret": "not_implemented"}) - elif sub == "db_remove": - return jsonify({"ret": "not_implemented"}) - elif sub == "add_whitelist": - try: - params = request.get_json() - logger.debug(f"add_whitelist params: {params}") - if params and "data_code" in params: - code = params["data_code"] - ret = LogicLinkkf.add_whitelist(code) - else: - ret = LogicLinkkf.add_whitelist() - return jsonify(ret) - except Exception as e: - logger.error(f"Exception: {e}") - logger.error(traceback.format_exc()) - return jsonify({"ret": False, "log": str(e)}) - elif sub == "command": - # command = queue_command와 동일 - cmd = request.form.get("cmd", "") - entity_id = request.form.get("entity_id", "") - - logger.debug(f"command endpoint - cmd: {cmd}, entity_id: {entity_id}") - - # list λͺ…λ Ή 처리 - if cmd == "list": - if self.queue: - return jsonify(self.queue.get_entity_list()) - else: - return jsonify([]) - - # 기타 λͺ…λ Ή 처리 - if self.queue: - ret = self.queue.command(cmd, int(entity_id) if entity_id else 0) - if ret is None: - ret = {"ret": "success"} - else: - ret = {"ret": "error", "log": "Queue not initialized"} + ret = ModelLinkkfItem.web_list(req) return jsonify(ret) - + elif sub == "db_remove": + db_id = request.form.get("id") + if not db_id: + return jsonify({"ret": "error", "log": "No ID provided"}) + return jsonify(ModelLinkkfItem.delete_by_id(db_id)) + + elif sub == "get_playlist": + # ν˜„μž¬ 파일과 같은 ν΄λ”μ—μ„œ λ‹€μŒ μ—ν”Όμ†Œλ“œλ“€ μ°ΎκΈ° + try: + file_path = request.args.get("path", "") + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found", "playlist": [], "current_index": 0}), 404 + + # λ³΄μ•ˆ 체크 + download_path = P.ModelSetting.get("linkkf_download_path") + if not file_path.startswith(download_path): + return jsonify({"error": "Access denied", "playlist": [], "current_index": 0}), 403 + + folder = os.path.dirname(file_path) + current_file = os.path.basename(file_path) + + # 파일λͺ…μ—μ„œ SxxExx νŒ¨ν„΄ μΆ”μΆœ + ep_match = re.search(r'\.S(\d+)E(\d+)\.', current_file, re.IGNORECASE) + if not ep_match: + # νŒ¨ν„΄ μ—†μœΌλ©΄ ν˜„μž¬ 파일만 λ°˜ν™˜ + return jsonify({ + "playlist": [{"path": file_path, "name": current_file}], + "current_index": 0 + }) + + current_season = int(ep_match.group(1)) + current_episode = int(ep_match.group(2)) + + # 같은 ν΄λ”μ˜ λͺ¨λ“  mp4 파일 κ°€μ Έμ˜€κΈ° + all_files = [] + for f in os.listdir(folder): + if f.endswith('.mp4'): + match = re.search(r'\.S(\d+)E(\d+)\.', f, re.IGNORECASE) + if match: + s = int(match.group(1)) + e = int(match.group(2)) + all_files.append({ + "path": os.path.join(folder, f), + "name": f, + "season": s, + "episode": e + }) + + # μ‹œμ¦Œ/μ—ν”Όμ†Œλ“œ 순으둜 μ •λ ¬ + all_files.sort(key=lambda x: (x["season"], x["episode"])) + + # ν˜„μž¬ μ—ν”Όμ†Œλ“œ 이상인 κ²ƒλ§Œ 필터링 (ν˜„μž¬ + λ‹€μŒ μ—ν”Όμ†Œλ“œλ“€) + playlist = [] + current_index = 0 + for i, f in enumerate(all_files): + if f["season"] == current_season and f["episode"] >= current_episode: + entry = {"path": f["path"], "name": f["name"]} + if f["episode"] == current_episode: + current_index = len(playlist) + playlist.append(entry) + + logger.info(f"Linkkf Playlist: {len(playlist)} items, current_index: {current_index}") + return jsonify({ + "playlist": playlist, + "current_index": current_index + }) + + except Exception as e: + logger.error(f"Get playlist error: {e}") + logger.error(traceback.format_exc()) + return jsonify({"error": str(e), "playlist": [], "current_index": 0}), 500 + + elif sub == "stream_video": + # λΉ„λ””μ˜€ 슀트리밍 (MP4 파일 직접 μ„œλΉ™) + try: + from flask import send_file, Response, make_response + import mimetypes + + file_path = request.args.get("path", "") + if not file_path or not os.path.exists(file_path): + return "File not found", 404 + + # λ³΄μ•ˆ 체크: λ‹€μš΄λ‘œλ“œ 경둜 내에 μžˆλŠ”μ§€ 확인 + download_path = P.ModelSetting.get("linkkf_download_path") + if not file_path.startswith(download_path): + return "Access denied", 403 + + file_size = os.path.getsize(file_path) + range_header = request.headers.get('Range', None) + + if not range_header: + return send_file(file_path, mimetype='video/mp4', as_attachment=False) + + # Range Request 처리 (seeking 지원) + byte1, byte2 = 0, None + m = re.search('(\d+)-(\d*)', range_header) + if m: + g = m.groups() + byte1 = int(g[0]) + if g[1]: + byte2 = int(g[1]) + + if byte2 is None: + byte2 = file_size - 1 + + length = byte2 - byte1 + 1 + + with open(file_path, 'rb') as f: + f.seek(byte1) + data = f.read(length) + + rv = Response(data, 206, mimetype='video/mp4', content_type='video/mp4', direct_passthrough=True) + rv.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(byte1, byte2, file_size)) + rv.headers.add('Accept-Ranges', 'bytes') + return rv + except Exception as e: + logger.error(f"Stream video error: {e}") + logger.error(traceback.format_exc()) + return jsonify({"error": str(e)}), 500 + # λ§€μΉ˜λ˜λŠ” subκ°€ μ—†λŠ” 경우 κΈ°λ³Έ 응닡 return jsonify({"ret": "error", "log": f"Unknown sub: {sub}"}) @@ -324,7 +408,7 @@ class LogicLinkkf(PluginModuleBase): queue νŽ˜μ΄μ§€μ—μ„œ list, stop λ“±μ˜ λͺ…령을 처리 """ ret = {"ret": "success"} - logger.debug(f"process_command - command: {command}, arg1: {arg1}") + # logger.debug(f"process_command - command: {command}, arg1: {arg1}") if command == "list": # 큐 λͺ©λ‘ λ°˜ν™˜ @@ -335,17 +419,37 @@ class LogicLinkkf(PluginModuleBase): return jsonify(ret) elif command == "stop": - # λ‹€μš΄λ‘œλ“œ 쀑지 + # λ‹€μš΄λ‘œλ“œ 쀑지 (cancel) if self.queue and arg1: try: entity_id = int(arg1) - result = self.queue.command("stop", entity_id) + result = self.queue.command("cancel", entity_id) if result: ret = result except Exception as e: ret = {"ret": "error", "log": str(e)} return jsonify(ret) + elif command == "remove": + # κ°œλ³„ ν•­λͺ© μ‚­μ œ + if self.queue and arg1: + try: + entity_id = int(arg1) + result = self.queue.command("remove", entity_id) + if result: + ret = result + except Exception as e: + ret = {"ret": "error", "log": str(e)} + return jsonify(ret) + + elif command in ["reset", "delete_completed"]: + # 전체 μ΄ˆκΈ°ν™” λ˜λŠ” μ™„λ£Œ μ‚­μ œ + if self.queue: + result = self.queue.command(command, 0) + if result: + ret = result + return jsonify(ret) + elif command == "queue_list": # λŒ€κΈ° 큐 λͺ©λ‘ if self.queue: @@ -934,7 +1038,7 @@ class LogicLinkkf(PluginModuleBase): data = {"ret": "success", "page": page} response_data = LogicLinkkf.get_html(url, timeout=10) # P.logger.debug(response_data) - P.logger.debug("debug.....................") + # P.logger.debug("debug.....................") # P.logger.debug(response_data) # JSON 응닡인지 확인 @@ -1182,6 +1286,7 @@ class LogicLinkkf(PluginModuleBase): "program_title": data["title"], "save_folder": Util.change_text_for_use_filename(data["save_folder"]), "title": ep_title, + "ep_num": ep_name, "season": data["season"], } @@ -1546,6 +1651,32 @@ class LinkkfQueueEntity(FfmpegQueueEntity): logger.error(traceback.format_exc()) self.url = playid_url + def download_completed(self): + """λ‹€μš΄λ‘œλ“œ μ™„λ£Œ ν›„ 처리 (파일 이동, DB μ—…λ°μ΄νŠΈ λ“±)""" + try: + logger.info(f"LinkkfQueueEntity.download_completed called for index {self.entity_id}") + + from framework import app + with app.app_context(): + # DB μƒνƒœ μ—…λ°μ΄νŠΈ + db_item = ModelLinkkfItem.get_by_linkkf_id(self.info.get("_id")) + if db_item: + db_item.status = "completed" + db_item.completed_time = datetime.now() + db_item.filepath = self.filepath + db_item.filename = self.filename + db_item.save() + logger.info(f"Updated DB status to 'completed' for episode {db_item.id}") + else: + logger.warning(f"Could not find DB item to update for _id {self.info.get('_id')}") + + # 전체 λͺ©λ‘ 갱신을 μœ„ν•΄ μ†ŒμΌ“IO λ°œμ‹  (ν•„μš” μ‹œ) + # from framework import socketio + # socketio.emit("linkkf_refresh", {"idx": self.entity_id}, namespace="/framework") + except Exception as e: + logger.error(f"Error in LinkkfQueueEntity.download_completed: {e}") + logger.error(traceback.format_exc()) + def refresh_status(self): try: # from framework import socketio (FlaskFarm ν‘œμ€€ 방식) @@ -1571,7 +1702,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity): # ν…œν”Œλ¦Ώμ΄ κΈ°λŒ€ν•˜λŠ” ν•„λ“œλ“€ μΆ”κ°€ tmp["idx"] = self.entity_id - tmp["callback_id"] = f"linkkf_{self.entity_id}" + tmp["callback_id"] = "linkkf" tmp["start_time"] = self.created_time.strftime("%m-%d %H:%M") if hasattr(self, 'created_time') and self.created_time and hasattr(self.created_time, 'strftime') else (self.created_time if self.created_time else "") tmp["status_kor"] = self.ffmpeg_status_kor if self.ffmpeg_status_kor else "λŒ€κΈ°μ€‘" tmp["percent"] = self.ffmpeg_percent if self.ffmpeg_percent else 0 @@ -1646,7 +1777,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity): continue # logger.debug(f"url: {url}, url2: {url2}") ret = LogicLinkkf.get_video_url_from_url(url, url2) - logger.debug(f"ret::::> {ret}") + # logger.debug(f"ret::::> {ret}") if ret is not None: video_url = ret @@ -1740,7 +1871,82 @@ class ModelLinkkfItem(db.Model): item.savepath = q["savepath"] item.video_url = q["url"] item.vtt_url = q["vtt"] - item.thumbnail = q["image"][0] + item.thumbnail = q.get("image", "") item.status = "wait" item.linkkf_info = q["linkkf_info"] item.save() + + @classmethod + def get_paging_info(cls, count, page, page_size): + total_page = int(count / page_size) + (1 if count % page_size != 0 else 0) + start_page = (int((page - 1) / 10)) * 10 + 1 + last_page = start_page + 9 + if last_page > total_page: + last_page = total_page + + ret = { + "start_page": start_page, + "last_page": last_page, + "total_page": total_page, + "current_page": page, + "count": count, + "page_size": page_size, + } + ret["prev_page"] = True if ret["start_page"] != 1 else False + ret["next_page"] = ( + True + if (ret["start_page"] + 10) <= ret["total_page"] + else False + ) + return ret + + @classmethod + def delete_by_id(cls, idx): + db.session.query(cls).filter_by(id=idx).delete() + db.session.commit() + return True + + @classmethod + def web_list(cls, req): + ret = {} + page = int(req.form["page"]) if "page" in req.form else 1 + page_size = 30 + job_id = "" + search = req.form["search_word"] if "search_word" in req.form else "" + option = req.form["option"] if "option" in req.form else "all" + order = req.form["order"] if "order" in req.form else "desc" + query = cls.make_query(search=search, order=order, option=option) + count = query.count() + query = query.limit(page_size).offset((page - 1) * page_size) + lists = query.all() + ret["list"] = [item.as_dict() for item in lists] + ret["paging"] = cls.get_paging_info(count, page, page_size) + return ret + + @classmethod + def make_query(cls, search="", order="desc", option="all"): + query = db.session.query(cls) + if search is not None and search != "": + if search.find("|") != -1: + tmp = search.split("|") + conditions = [] + for tt in tmp: + if tt != "": + conditions.append(cls.filename.like("%" + tt.strip() + "%")) + query = query.filter(or_(*conditions)) + elif search.find(",") != -1: + tmp = search.split(",") + for tt in tmp: + if tt != "": + query = query.filter(cls.filename.like("%" + tt.strip() + "%")) + else: + query = query.filter(cls.filename.like("%" + search + f"%")) + + if option == "completed": + query = query.filter(cls.status == "completed") + + if order == "desc": + query = query.order_by(desc(cls.id)) + else: + query = query.order_by(cls.id) + return query diff --git a/mod_ohli24.py b/mod_ohli24.py index 4bd3ee5..cc2be21 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -136,6 +136,7 @@ class LogicOhli24(PluginModuleBase): } self.queue = None # default_route_socketio(P, self) + self.web_list_model = ModelOhli24Item default_route_socketio_module(self, attach="/queue") def cleanup_stale_temps(self) -> None: @@ -300,7 +301,10 @@ class LogicOhli24(PluginModuleBase): return jsonify(ModelOhli24Item.web_list(request)) elif sub == "db_remove": - return jsonify(ModelOhli24Item.delete_by_id(req.form["id"])) + db_id = request.form.get("id") + if not db_id: + return jsonify({"ret": "error", "log": "No ID provided"}) + return jsonify(ModelOhli24Item.delete_by_id(db_id)) elif sub == "add_whitelist": try: # params = request.get_data() @@ -318,6 +322,7 @@ class LogicOhli24(PluginModuleBase): except Exception as e: logger.error(f"Exception: {e}") logger.error(traceback.format_exc()) + return jsonify({"error": str(e)}), 500 elif sub == "stream_video": # λΉ„λ””μ˜€ 슀트리밍 (MP4 파일 직접 μ„œλΉ™) @@ -451,6 +456,10 @@ class LogicOhli24(PluginModuleBase): except Exception as e: P.logger.error(f"Exception: {e}") P.logger.error(traceback.format_exc()) + return jsonify({"error": str(e)}), 500 + + # λ§€μΉ­λ˜μ§€ μ•ŠλŠ” sub μš”μ²­μ— λŒ€ν•œ κΈ°λ³Έ 응닡 + return jsonify({"error": f"Unknown sub: {sub}"}), 404 def get_episode(self, clip_id): for _ in self.current_data["episode"]: diff --git a/static/js/sjva_global1.js b/static/js/sjva_global1.js index 1fda6f4..cf7fd6c 100644 --- a/static/js/sjva_global1.js +++ b/static/js/sjva_global1.js @@ -47,7 +47,7 @@ function get_formdata(form_id) { } function globalRequestSearch2(page, move_top = true) { - var formData = getFormdata("#form_search") + var formData = get_formdata("#form_search") formData += "&page=" + page console.log(formData) $.ajax({ diff --git a/templates/anime_downloader_linkkf_list.html b/templates/anime_downloader_linkkf_list.html index 1d3ec23..22a5a44 100644 --- a/templates/anime_downloader_linkkf_list.html +++ b/templates/anime_downloader_linkkf_list.html @@ -1,279 +1,512 @@ {% extends "base.html" %} {% block content %} - -
- - - - - + + + - - {% endblock %} \ No newline at end of file diff --git a/templates/anime_downloader_linkkf_queue.html b/templates/anime_downloader_linkkf_queue.html index 946eddf..603885b 100644 --- a/templates/anime_downloader_linkkf_queue.html +++ b/templates/anime_downloader_linkkf_queue.html @@ -1,503 +1,407 @@ {% extends "base.html" %} {% block content %} - -
- - - - - - - - - - - - - - - - - -
IDXPluginμ‹œμž‘μ‹œκ°„νŒŒμΌλͺ…μƒνƒœμ§„ν–‰λ₯ κΈΈμ΄PFλ°°μ†μ§„ν–‰μ‹œκ°„Action
+
+

λ‹€μš΄λ‘œλ“œ 큐

+
+ + +
+
μ‹€μ‹œκ°„ 동기화 ν™œμ„±ν™”λ¨ (3초 μ£ΌκΈ°)
+
+ +
+ + + + + + + + + + + + + + + + +
IDXPluginμ‹œμž‘μ‹œκ°„νŒŒμΌλͺ…μƒνƒœμ§„ν–‰λ₯ κΈΈμ΄PFν˜„μž¬ μƒνƒœAction
+
+
+ -
- - - + {% endblock %} + diff --git a/templates/anime_downloader_linkkf_request.html b/templates/anime_downloader_linkkf_request.html index d975b90..a6097b0 100644 --- a/templates/anime_downloader_linkkf_request.html +++ b/templates/anime_downloader_linkkf_request.html @@ -21,12 +21,19 @@ -
-
- {{ macros.setting_input_text_and_buttons('code', 'μž‘ν’ˆ Code', - [['analysis_btn', '뢄석'], ['go_linkkf_btn', 'Go 링크 μ• λ‹ˆ']], desc='예) - "https://linkkf.app/μ½”λ“œ" λ‚˜ "μ½”λ“œ"') }} -
+
+
μž‘ν’ˆ URL λ˜λŠ” μ½”λ“œλ‘œ λΆ„μ„ν•˜κΈ°
+
+ +
+ + +
+
+
+ 예) "https://linkkf.app/μ½”λ“œ" λ‚˜ "μ½”λ“œ" 직접 μž…λ ₯ +
+
@@ -39,7 +46,7 @@ const package_name = "{{arg['package_name'] }}"; const sub = "{{arg['sub'] }}"; const ohli24_url = "{{arg['ohli24_url']}}"; - {#let current_data = '';#} + // let current_data = ''; const params = new Proxy(new URLSearchParams(window.location.search), { get: (searchParams, prop) => searchParams.get(prop), @@ -163,7 +170,7 @@ for (let i in data.episode) { str += '
'; str += '
'; - str += '' + (parseInt(i) + 1) + 'ν™”'; + str += '' + data.episode[i].ep_num + 'ν™”'; str += '
'; str += '
'; str += '
' + data.episode[i].title + '
'; @@ -348,13 +355,137 @@ } body { - background-image: linear-gradient(90deg, #33242c, #263341, #17273a); - + background-image: linear-gradient(90deg, #022c22, #064e3b, #065f46); } #anime_downloader_wrapper { + color: #ecfdf5; + padding: 5px; + } - color: #d6eaf8; + /* Navigation (Tabs) Optimization */ + .nav-pills { + background: rgba(6, 78, 59, 0.4) !important; + padding: 6px !important; + border-radius: 12px !important; + border: 1px solid rgba(16, 185, 129, 0.1) !important; + margin-bottom: 20px !important; + display: inline-flex !important; + gap: 4px !important; + box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important; + } + .nav-pills .nav-link { + color: #d1fae5 !important; + font-weight: 600 !important; + padding: 8px 20px !important; + border-radius: 8px !important; + transition: all 0.3s ease !important; + border: 1px solid transparent !important; + } + .nav-pills .nav-link:hover { + background: rgba(16, 185, 129, 0.1) !important; + color: #fff !important; + transform: translateY(-1px); + } + .nav-pills .nav-link.active { + background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; + color: #fff !important; + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important; + border: 1px solid rgba(255,255,255,0.1) !important; + } + + /* Search Input Polishing */ + .search-input-container { + background: rgba(6, 78, 59, 0.4); + backdrop-filter: blur(12px); + padding: 24px; + border-radius: 20px; + border: 1px solid rgba(16, 185, 129, 0.2); + margin-bottom: 30px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + } + + .search-label { + font-size: 16px; + font-weight: 700; + color: #ecfdf5; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; + } + + .search-input-wrapper { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .custom-input { + background-color: rgba(2, 44, 34, 0.6) !important; + border: 1px solid rgba(16, 185, 129, 0.3) !important; + color: #fff !important; + border-radius: 12px !important; + padding: 12px 18px !important; + height: 50px !important; + flex: 1; + min-width: 300px; + font-size: 15px; + transition: all 0.2s; + } + + .custom-input:focus { + border-color: #10b981 !important; + box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2) !important; + background-color: rgba(2, 44, 34, 0.8) !important; + } + + .search-buttons { + display: flex; + gap: 10px; + } + + .custom-btn { + height: 50px; + padding: 0 24px; + border-radius: 12px; + font-weight: 700; + cursor: pointer; + transition: all 0.2s; + border: none; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + } + + .btn-search { + background: linear-gradient(135deg, #10b981, #059669); + color: white; + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); + } + + .btn-search:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4); + } + + .btn-reset { + background: rgba(255, 255, 255, 0.05); + color: #6ee7b7; + border: 1px solid rgba(16, 185, 129, 0.2); + } + + .btn-reset:hover { + background: rgba(255, 255, 255, 0.1); + color: #fff; + } + + .search-desc { + margin-top: 12px; + font-size: 13px; + color: #6ee7b7; + opacity: 0.7; } button.code-button { @@ -456,16 +587,14 @@ .card { border: none; - box-shadow: inset 1px 1px hsl(0deg 0% 100% / 20%), inset -1px -1px hsl(0deg 0% 100% / 10%), 1px 3px 24px -1px rgb(0 0 0 / 15%); - background-color: transparent; - background-image: linear-gradient(125deg, hsla(0, 0%, 100%, .3), hsla(0, 0%, 100%, .2) 70%); - backdrop-filter: blur(5px); + box-shadow: inset 1px 1px hsl(0deg 0% 100% / 10%), inset -1px -1px hsl(0deg 0% 100% / 5%), 0 12px 24px rgba(0,0,0,0.3); + background-color: rgba(6, 78, 59, 0.4); + backdrop-filter: blur(12px); } .card.border-light { - border-radius: 30px 10px; - --bs-border-opacity: 1; - border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; + border-radius: 20px; + border: 1px solid rgba(16, 185, 129, 0.2) !important; } /* μ—ν”Όμ†Œλ“œ λͺ©λ‘ μ»¨ν…Œμ΄λ„ˆ */ @@ -504,7 +633,7 @@ height: 40px; border-radius: 8px; overflow: hidden; - background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + background: linear-gradient(135deg, #10b981 0%, #059669 100%); display: flex; align-items: center; justify-content: center; diff --git a/templates/anime_downloader_linkkf_search.html b/templates/anime_downloader_linkkf_search.html index 06a12a8..ef02ff3 100644 --- a/templates/anime_downloader_linkkf_search.html +++ b/templates/anime_downloader_linkkf_search.html @@ -359,11 +359,11 @@ // tmp += // ''; tmp += - '"; + `'"/>`; if (current_cate === "ing") { tmp += '' + @@ -476,7 +476,7 @@ case "ing": // console.log("ing.....") - {#spinner_loading.style.display = "block";#} + // spinner_loading.style.display = "block"; current_cate = "ing"; get_anime_list(1, "ing"); break; @@ -680,7 +680,7 @@ console.dir(e.target.scrollingElement.scrollHeight); const {scrollTop, scrollHeight, clientHeight} = e.target.scrollingElement; if (Math.round(scrollHeight - scrollTop) <= clientHeight + 170) { - {#document.getElementById("spinner").style.display = "block";#} + // document.getElementById("spinner").style.display = "block"; // console.log("loading"); // console.log("now page::> ", page); // console.log("next_page::> ", String(next_page)); @@ -796,22 +796,22 @@ diff --git a/templates/anime_downloader_linkkf_setting.html b/templates/anime_downloader_linkkf_setting.html index 3235df8..c65f5f7 100644 --- a/templates/anime_downloader_linkkf_setting.html +++ b/templates/anime_downloader_linkkf_setting.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block content %} -
+
@@ -24,8 +24,9 @@ {{ macros.m_tab_content_start('normal', true) }} {{ macros.setting_input_text_and_buttons('linkkf_url', 'linkkf URL', [['go_btn', 'GO']], value=arg['linkkf_url']) }} {{ macros.setting_input_text('linkkf_download_path', 'μ €μž₯ 폴더', value=arg['linkkf_download_path'], desc='μ •μƒμ μœΌλ‘œ λ‹€μš΄ μ™„λ£Œ 된 파일이 이동할 폴더 μž…λ‹ˆλ‹€. ') }} - {{ macros.setting_input_int('linkkf_max_ffmpeg_process_count', 'λ™μ‹œ λ‹€μš΄λ‘œλ“œ 수', value=arg['linkkf_max_ffmpeg_process_count'], desc='λ™μ‹œμ— λ‹€μš΄λ‘œλ“œ ν•  μ—ν”Όμ†Œλ“œ κ°―μˆ˜μž…λ‹ˆλ‹€.') }} - {{ macros.setting_select('linkkf_download_method', 'λ‹€μš΄λ‘œλ“œ 방법', [['ffmpeg', 'ffmpeg'], ['ytdlp', 'yt-dlp']], col='3', value=arg['linkkf_download_method'], desc='ffmpeg: HLS λ‹€μš΄λ‘œλ” μ‚¬μš©, yt-dlp: yt-dlp μ‚¬μš©') }} + {{ macros.setting_input_int('linkkf_max_ffmpeg_process_count', 'λ™μ‹œ λ‹€μš΄λ‘œλ“œ μ—ν”Όμ†Œλ“œ 수', value=arg['linkkf_max_ffmpeg_process_count'], desc='λ™μ‹œμ— λ‹€μš΄λ‘œλ“œν•  μ—ν”Όμ†Œλ“œ κ°œμˆ˜μž…λ‹ˆλ‹€.') }} + {{ macros.setting_select('linkkf_download_method', 'λ‹€μš΄λ‘œλ“œ 방법', [['ffmpeg', 'ffmpeg (κΈ°λ³Έ)'], ['ytdlp', 'yt-dlp (λ‹¨μΌμ“°λ ˆλ“œ)'], ['aria2c', 'yt-dlp (λ©€ν‹°μ“°λ ˆλ“œ/aria2c)']], col='3', value=arg['linkkf_download_method'], desc='aria2c 선택 μ‹œ 병렬 λ‹€μš΄λ‘œλ“œλ‘œ 속도가 ν–₯μƒλ©λ‹ˆλ‹€.') }} + {{ macros.setting_input_int('linkkf_download_threads', 'λ©€ν‹°μ“°λ ˆλ“œ 갯수', value=arg['linkkf_download_threads'], desc='yt-dlp/aria2c μ‚¬μš© μ‹œ 적용될 병렬 λ‹€μš΄λ‘œλ“œ μ“°λ ˆλ“œ μˆ˜μž…λ‹ˆλ‹€. (κΈ°λ³Έ 16)') }} {{ macros.setting_checkbox('linkkf_order_desc', 'μš”μ²­ ν™”λ©΄ μ΅œμ‹ μˆœ μ •λ ¬', value=arg['linkkf_order_desc'], desc='On : μ΅œμ‹ ν™”λΆ€ν„°, Off : 1ν™”λΆ€ν„°') }} {{ macros.setting_checkbox('linkkf_auto_make_folder', '제λͺ© 폴더 생성', value=arg['linkkf_auto_make_folder'], desc='제λͺ©μœΌλ‘œ 폴더λ₯Ό μƒμ„±ν•˜κ³  폴더 μ•ˆμ— λ‹€μš΄λ‘œλ“œν•©λ‹ˆλ‹€.') }}
@@ -108,28 +109,35 @@ margin-bottom: 20px; } - ul.nav.nav-pills .nav-item { - margin: 0 2px; + /* Navigation (Tabs) Optimization */ + .nav-pills { + background: rgba(6, 78, 59, 0.4) !important; + padding: 6px !important; + border-radius: 12px !important; + border: 1px solid rgba(16, 185, 129, 0.1) !important; + margin-bottom: 20px !important; + display: inline-flex !important; + gap: 4px !important; + box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important; } - - ul.nav.nav-pills .nav-link { - border-radius: 50rem !important; - padding: 8px 20px !important; - color: #94a3b8 !important; /* Muted text */ - font-weight: 600; - transition: all 0.3s ease; + .nav-pills .nav-link { + color: #d1fae5 !important; + font-weight: 600 !important; + padding: 8px 18px !important; + border-radius: 8px !important; + transition: all 0.3s ease !important; + border: 1px solid transparent !important; } - - ul.nav.nav-pills .nav-link:hover { - background-color: rgba(255, 255, 255, 0.1); + .nav-pills .nav-link:hover { + background: rgba(16, 185, 129, 0.1) !important; color: #fff !important; transform: translateY(-1px); } - - ul.nav.nav-pills .nav-link.active { - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important; + .nav-pills .nav-link.active { + background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important; color: #fff !important; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3) !important; + border: 1px solid rgba(255,255,255,0.1) !important; } /* Form Controls */ @@ -142,8 +150,8 @@ .form-control:focus, .custom-select:focus, textarea:focus { background-color: rgba(0, 0, 0, 0.5) !important; - border-color: #3b82f6 !important; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.25) !important; + border-color: #10b981 !important; + box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25) !important; } /* Labels & Text */ @@ -168,15 +176,15 @@ } .btn-primary, #globalSettingSaveBtn { - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); + background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; - box-shadow: 0 4px 15px rgba(37, 99, 235, 0.4); + box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4); } .btn-primary:hover, #globalSettingSaveBtn:hover { - background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); + background: linear-gradient(135deg, #34d399 0%, #10b981 100%); transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(37, 99, 235, 0.6); + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.6); } /* GO Button specific (Input Group) */ @@ -218,8 +226,8 @@ border-color: rgba(255,255,255,0.2); } .custom-control-input:checked ~ .custom-control-label::before { - background-color: #3b82f6; - border-color: #3b82f6; + background-color: #10b981; + border-color: #10b981; } /* Collapse Borders */ diff --git a/templates/anime_downloader_ohli24_list.html b/templates/anime_downloader_ohli24_list.html index 5187150..c4ff8ef 100644 --- a/templates/anime_downloader_ohli24_list.html +++ b/templates/anime_downloader_ohli24_list.html @@ -31,8 +31,28 @@
+ + + - + + @@ -128,7 +148,7 @@ function global_sub_request_search(page, move_top = true) { console.log('........................') - var formData = getFormdata('#form_search') + var formData = get_formdata('#form_search') formData += '&page=' + page; $.ajax({ url: '/' + package_name + '/ajax/' + sub + '/web_list3', @@ -150,8 +170,7 @@ $("#search").click(function (e) { e.preventDefault(); - {#global_sub_request_search('1');#} - globalRequestSearch('1'); + global_sub_request_search('1'); }); $("body").on('click', '#page', function (e) { @@ -185,18 +204,27 @@ global_sub_request_search('1') }); + var targetDeleteId = null; + $("body").on('click', '.btn-remove', function (e) { e.preventDefault(); - id = $(this).data('id'); + targetDeleteId = $(this).data('id'); + $('#confirmModal').modal('show'); + }); + + $('#confirmDeleteBtn').click(function() { + if (!targetDeleteId) return; + $.ajax({ url: '/' + package_name + '/ajax/' + sub + '/db_remove', type: "POST", cache: false, - data: {id: id}, + data: {id: targetDeleteId}, dataType: "json", success: function (data) { + $('#confirmModal').modal('hide'); if (data) { - $.notify('μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', { + $.notify('μ„±κ³΅μ μœΌλ‘œ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.', { type: 'success' }); global_sub_request_search(current_data.paging.current_page, false)