diff --git a/info.yaml b/info.yaml index 65604b2..c484c84 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "애니 다운로더" -version: "0.4.1" +version: "0.4.2" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/lib/util.py b/lib/util.py index c327264..410cb0f 100644 --- a/lib/util.py +++ b/lib/util.py @@ -110,7 +110,7 @@ class Util(object): i = 0 while i < len(lines): line = lines[i].strip() - # WEBVTT, NOTE, STYLE 등 메타데이터 스킵 + # WEBWTT, NOTE, STYLE 등 메타데이터 스킵 if line.startswith("WEBVTT") or line.startswith("NOTE") or line.startswith("STYLE"): i += 1 continue @@ -135,3 +135,50 @@ class Util(object): # 캡션 텍스트가 바로 나오는 경우 등을 대비 i += 1 return "\n".join(srt_lines) + + @staticmethod + def merge_subtitle(P, db_item): + """ + ffmpeg를 사용하여 SRT 자막을 MP4에 삽입 (soft embed) + """ + try: + import subprocess + mp4_path = db_item.filepath + if not mp4_path or not os.path.exists(mp4_path): + logger.error(f"MP4 file not found: {mp4_path}") + return + + srt_path = os.path.splitext(mp4_path)[0] + ".srt" + if not os.path.exists(srt_path): + logger.error(f"SRT file not found: {srt_path}") + return + + # 출력 파일: *_subed.mp4 + base_name = os.path.splitext(mp4_path)[0] + output_path = f"{base_name}_subed.mp4" + + if os.path.exists(output_path): + os.remove(output_path) + + ffmpeg_cmd = [ + "ffmpeg", "-y", + "-i", mp4_path, + "-i", srt_path, + "-c:v", "copy", + "-c:a", "copy", + "-c:s", "mov_text", + "-metadata:s:s:0", "language=kor", + output_path + ] + + logger.info(f"[Merge Subtitle] Running ffmpeg: {' '.join(ffmpeg_cmd)}") + result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=600) + + if result.returncode == 0 and os.path.exists(output_path): + logger.info(f"[Merge Subtitle] Success: {output_path}") + # 원본 삭제 옵션 등이 필요할 수 있으나 여기서는 생성만 함 + else: + logger.error(f"ffmpeg failed: {result.stderr}") + except Exception as e: + logger.error(f"merge_subtitle error: {e}") + logger.error(traceback.format_exc()) diff --git a/mod_anilife.py b/mod_anilife.py index dd94ff5..cd86c4d 100644 --- a/mod_anilife.py +++ b/mod_anilife.py @@ -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() diff --git a/mod_base.py b/mod_base.py index 77bb6a5..5697d85 100644 --- a/mod_base.py +++ b/mod_base.py @@ -131,6 +131,8 @@ class AnimeModuleBase(PluginModuleBase): arg3 = request.form.get('arg3') or request.args.get('arg3') return self.process_command(command, arg1, arg2, arg3, req) + return jsonify({'ret': 'fail', 'log': f"Unknown sub: {sub}"}) + except Exception as e: self.P.logger.error(f"AJAX Error: {e}") self.P.logger.error(traceback.format_exc()) diff --git a/mod_linkkf.py b/mod_linkkf.py index d43f9c0..8c461c7 100644 --- a/mod_linkkf.py +++ b/mod_linkkf.py @@ -1691,9 +1691,10 @@ class LinkkfQueueEntity(FfmpegQueueEntity): """ from .lib.downloader_factory import DownloaderFactory - # 설정에서 다운로드 방식 읽기 + # 설정에서 다운로드 방식 및 쓰레드 수 읽기 method = self.P.ModelSetting.get("linkkf_download_method") or "ytdlp" - logger.info(f"Linkkf get_downloader using method: {method}") + threads = self.P.ModelSetting.get_int("linkkf_download_threads") or 16 + logger.info(f"Linkkf get_downloader using method: {method}, threads: {threads}") return DownloaderFactory.get_downloader( method=method, @@ -1702,6 +1703,7 @@ class LinkkfQueueEntity(FfmpegQueueEntity): headers=self.headers, callback=callback, callback_id="linkkf", + threads=threads, callback_function=callback_function ) diff --git a/templates/anime_downloader_anilife_category.html b/templates/anime_downloader_anilife_category.html index 78bd2e8..274e5ca 100644 --- a/templates/anime_downloader_anilife_category.html +++ b/templates/anime_downloader_anilife_category.html @@ -681,12 +681,119 @@ background-color: #e0ff42; } + /* ========== Cosmic Violet Theme (Anilife Exclusive) ========== */ body { - font-family: NanumSquareNeo, system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Noto Sans, Liberation Sans, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + font-family: 'Inter', 'Noto Sans KR', system-ui, sans-serif; + background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #4c1d95 100%) !important; + background-attachment: fixed; + color: #e0e7ff; + min-height: 100vh; } - body { - background-image: linear-gradient(90deg, #233f48, #6c6fa2, #768dae); + /* Search Bar Styling */ + .input-group { + background: rgba(49, 46, 129, 0.5); + border-radius: 12px; + padding: 6px; + border: 1px solid rgba(167, 139, 250, 0.25); + backdrop-filter: blur(10px); + margin-bottom: 15px; + } + + #input_search { + background: rgba(30, 27, 75, 0.7) !important; + border: 1px solid rgba(167, 139, 250, 0.2) !important; + color: #e0e7ff !important; + border-radius: 8px !important; + } + + #input_search::placeholder { + color: #c4b5fd; + opacity: 0.7; + } + + #btn_search { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important; + border: none !important; + border-radius: 8px !important; + } + + /* Category Buttons */ + #anime_category { + margin: 15px 0; + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + #anime_category .btn { + background: rgba(49, 46, 129, 0.5) !important; + border: 1px solid rgba(167, 139, 250, 0.3) !important; + color: #c4b5fd !important; + border-radius: 20px !important; + padding: 8px 20px !important; + } + + #anime_category .btn:hover { + background: rgba(139, 92, 246, 0.3) !important; + color: #fff !important; + } + + #anime_category .btn-success { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important; + color: white !important; + } + + #anime_category .btn-primary { + background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%) !important; + color: white !important; + } + + #anime_category .btn-grey { + background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%) !important; + color: white !important; + } + + /* Card Styling */ + .card { + background: rgba(49, 46, 129, 0.5) !important; + border: 1px solid rgba(167, 139, 250, 0.15) !important; + border-radius: 12px !important; + overflow: hidden; + } + + .card:hover { + border-color: rgba(167, 139, 250, 0.4) !important; + box-shadow: 0 8px 30px rgba(139, 92, 246, 0.2) !important; + } + + .card-body { + background: rgba(30, 27, 75, 0.85) !important; + } + + .card-title { + color: #a78bfa !important; + } + + .card-text { + color: #c4b5fd !important; + } + + .card .btn-primary { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%) !important; + border: none !important; + } + + /* Page Badge */ + .btn-info { + background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%) !important; + border: none !important; + border-radius: 8px !important; + } + + /* Spinner */ + #spinner { + color: #a78bfa; } .demo { @@ -809,5 +916,21 @@ z-index: 99999; opacity: 0.5; } + + /* Mobile Responsive */ + @media (max-width: 768px) { + body { padding-top: 10px !important; } + ul.nav.nav-pills.bg-light { + margin-top: 50px !important; + margin-bottom: 10px !important; + width: 100% !important; + display: flex !important; + border-radius: 12px !important; + } + ul.nav.nav-pills .nav-link { + padding: 6px 12px !important; + font-size: 13px; + } + } {% endblock %} diff --git a/templates/anime_downloader_anilife_list.html b/templates/anime_downloader_anilife_list.html index 3845fd0..f8b4aca 100644 --- a/templates/anime_downloader_anilife_list.html +++ b/templates/anime_downloader_anilife_list.html @@ -1,305 +1,662 @@ {% extends "base.html" %} {% block content %} - + + + + +