diff --git a/README.md b/README.md
index b6ab44e..d6b2d1f 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,17 @@
## ๐ ๋ณ๊ฒฝ ์ด๋ ฅ (Changelog)
+### v0.4.13 (2026-01-02)
+- **Ohli24 CSS ์ฌ์ด๋ ์ดํํธ ์์ **:
+ - ํ์ด์ง๋ณ ๋
๋ฆฝ wrapper (`.ohli24-list-page`, `.ohli24-request-page`, `.ohli24-queue-page`) ์ ์ฉ์ผ๋ก ์คํ์ผ ๊ฐ์ญ ์์ ์ฐจ๋จ
+ - ์์ฒญ(Request) ํ์ด์ง์ ์ํผ์๋ ์นด๋ ๊ฐ๋ก ์ ๋ ฌ ๋ ์ด์์ ๋ณต๊ตฌ ๋ฐ ์ต์ ํ
+ - ์์ฒญ ํ์ด์ง ๋ด ๋ถํ์ํ ์ธ๋ผ์ธ ์คํ์ผ ์ ๊ฑฐ ๋ฐ ์ธ๋ถ CSS๋ก ์ผ์ํ
+- **์์ ์ฑ ๊ฐํ**:
+ - **Ohli24**: ํ ์ถ๊ฐ ์ ์ฆ์ ๋ฉํ๋ฐ์ดํฐ ํ์ฑ ๋ฐ DB ๋๊ธฐํ ๋ก์ง ๊ฐํ
+ - **FfmpegQueue**: ๋ก์ปฌ ํ์ผ ์กด์ฌ ์ DB ์ํ ๋๊ธฐํ ๋๋ฝ ์์
+ - **๊ฒ์**: Ohli24 ๊ฒ์ ๊ฒฐ๊ณผ ์ ํจ์ฑ ์ฒดํฌ ๋ก์ง ์ถ๊ฐ๋ก ๋ฐํ์ ์ค๋ฅ ๋ฐฉ์ง
+- **๋ชจ๋ ๊ฒ์ ์ง์**: `model_base.py` ๋ด Ohli24 ๋ฐ Linkkf ์ ์ฉ ๊ฒ์ ์ง์ ์ถ๊ฐ
+
### v0.4.5 (2026-01-02)
- **CSS ํ
๋ง ์ํคํ
์ฒ ์ ๋ฉด ๊ฐํธ**:
- ์ฌ์ดํธ๋ณ ๋
๋ฆฝ ํ
๋ง ํ์ผ ๋ถ๋ฆฌ (`anilife.css`, `linkkf.css`, `ohli24.css`)
diff --git a/info.yaml b/info.yaml
index 4e2b19b..47ed7bf 100644
--- a/info.yaml
+++ b/info.yaml
@@ -1,5 +1,5 @@
title: "์ ๋ ๋ค์ด๋ก๋"
-version: "0.4.12"
+version: "0.4.13"
package_name: "anime_downloader"
developer: "projectdx"
description: "anime downloader"
diff --git a/lib/ffmpeg_queue_v1.py b/lib/ffmpeg_queue_v1.py
index f4e43d3..1311750 100644
--- a/lib/ffmpeg_queue_v1.py
+++ b/lib/ffmpeg_queue_v1.py
@@ -228,8 +228,11 @@ class FfmpegQueue(object):
has_ytdl_file = os.path.exists(filepath + ".ytdl")
if os.path.exists(filepath) and not (is_ytdlp or has_ytdl_file):
+ logger.info(f"File already exists: {filepath}")
+ entity.ffmpeg_status = 8 # COMPLETED_EXIST
entity.ffmpeg_status_kor = "ํ์ผ ์์"
entity.ffmpeg_percent = 100
+ entity.download_completed()
entity.refresh_status()
continue
dirname = os.path.dirname(filepath)
diff --git a/mod_anilife.py b/mod_anilife.py
index 892dcb1..3b77b37 100644
--- a/mod_anilife.py
+++ b/mod_anilife.py
@@ -1718,6 +1718,22 @@ class AniLifeQueueEntity(FfmpegQueueEntity):
if not os.path.exists(self.savepath):
os.makedirs(self.savepath)
+ # [IMMEDIATE SYNC] Update DB with extracted metadata
+ try:
+ db_entity = ModelAniLifeItem.get_by_anilife_id(self.info["_id"])
+ if db_entity:
+ logger.debug(f"[SYNC] Syncing metadata for AniLife _id: {self.info.get('_id')}")
+ db_entity.title = self.content_title
+ db_entity.season = self.season
+ db_entity.episode_no = self.epi_queue
+ db_entity.savepath = self.savepath
+ db_entity.filename = self.filename
+ db_entity.filepath = self.filepath
+ db_entity.quality = self.quality
+ db_entity.save()
+ except Exception as sync_err:
+ logger.error(f"Failed to sync metadata to DB: {sync_err}")
+
# ์ต์ข
๋น๋์ค URL ์ค์
self.url = vod_url
logger.info(f"Final video URL: {self.url}")
diff --git a/mod_linkkf.py b/mod_linkkf.py
index 7b18bc2..80b8680 100644
--- a/mod_linkkf.py
+++ b/mod_linkkf.py
@@ -463,7 +463,7 @@ class LogicLinkkf(AnimeModuleBase):
logger.error(f"browse_dir error: {e}")
return jsonify({"ret": "error", "error": str(e)}), 500
- return jsonify({"ret": "error", "log": f"Unknown sub: {sub}"})
+ return super().process_ajax(sub, req)
except Exception as e:
P.logger.error(f"Exception: {str(e)}")
diff --git a/mod_ohli24.py b/mod_ohli24.py
index b323b2f..498432a 100644
--- a/mod_ohli24.py
+++ b/mod_ohli24.py
@@ -181,10 +181,6 @@ class LogicOhli24(AnimeModuleBase):
# @staticmethod
def process_ajax(self, sub: str, req: Any) -> Any:
try:
- ret = super().process_ajax(sub, req)
- if ret: return ret
-
- data = []
cate = request.form.get("type", None)
page = request.form.get("page", None)
@@ -199,22 +195,23 @@ class LogicOhli24(AnimeModuleBase):
self.current_data = data
return jsonify({"ret": "success", "data": data, "code": code})
elif sub == "anime_list":
-
data = self.get_anime_info(cate, page)
+ if isinstance(data, dict) and data.get("ret") == "error":
+ return jsonify(data)
return jsonify({"ret": "success", "cate": cate, "page": page, "data": data})
elif sub == "complete_list":
-
logger.debug("cate:: %s", cate)
page = request.form["page"]
-
data = self.get_anime_info(cate, page)
+ if isinstance(data, dict) and data.get("ret") == "error":
+ return jsonify(data)
return jsonify({"ret": "success", "cate": cate, "page": page, "data": data})
elif sub == "search":
-
query = request.form["query"]
page = request.form["page"]
-
data = self.get_search_result(query, page, cate)
+ if isinstance(data, dict) and data.get("ret") == "error":
+ return jsonify(data)
return jsonify(
{
"ret": "success",
@@ -499,8 +496,8 @@ class LogicOhli24(AnimeModuleBase):
logger.error(f"browse_dir error: {e}")
return jsonify({"ret": "error", "error": str(e)}), 500
- # ๋งค์นญ๋์ง ์๋ sub ์์ฒญ์ ๋ํ ๊ธฐ๋ณธ ์๋ต
- return jsonify({"error": f"Unknown sub: {sub}"}), 404
+ # Fallback to base class for common subs (setting_save, queue_command, entity_list, etc.)
+ return super().process_ajax(sub, req)
def get_episode(self, clip_id):
for _ in self.current_data["episode"]:
@@ -965,7 +962,7 @@ class LogicOhli24(AnimeModuleBase):
except Exception as e:
P.logger.error("Exception:%s", e)
P.logger.error(traceback.format_exc())
- return {"ret": "exception", "log": str(e)}
+ return {"ret": "error", "log": str(e)}
def get_anime_info(self, cate, page):
print(cate, page)
@@ -1010,7 +1007,7 @@ class LogicOhli24(AnimeModuleBase):
except Exception as e:
P.logger.error("Exception:%s", e)
P.logger.error(traceback.format_exc())
- return {"ret": "exception", "log": str(e)}
+ return {"ret": "error", "log": str(e)}
def get_auto_anime_info(self, url: str = ""):
try:
@@ -1038,7 +1035,7 @@ class LogicOhli24(AnimeModuleBase):
except Exception as e:
P.logger.error("Exception:%s", e)
P.logger.error(traceback.format_exc())
- return {"ret": "exception", "log": str(e)}
+ return {"ret": "error", "log": str(e)}
# @staticmethod
def get_search_result(self, query, page, cate):
@@ -1089,7 +1086,7 @@ class LogicOhli24(AnimeModuleBase):
except Exception as e:
P.logger.error(f"Exception: {str(e)}")
P.logger.error(traceback.format_exc())
- return {"ret": "exception", "log": str(e)}
+ return {"ret": "error", "log": str(e)}
def process_api(self, sub: str, req: Any) -> Any:
try:
@@ -1596,18 +1593,61 @@ class Ohli24QueueEntity(AnimeQueueEntity):
self.epi_queue: Optional[str] = None
self.filepath: Optional[str] = None
self.savepath: Optional[str] = None
- self.quality: Optional[str] = None
+ self.quality: Optional[str] = "720P"
self.filename: Optional[str] = None
self.vtt: Optional[str] = None
self.season: int = 1
self.content_title: Optional[str] = None
self.srt_url: Optional[str] = None
self.headers: Optional[Dict[str, str]] = None
- self.cookies_file: Optional[str] = None # yt-dlp์ฉ CDN ์ธ์
์ฟ ํค ํ์ผ ๊ฒฝ๋ก
- self.need_special_downloader: bool = False # CDN ๋ณด์ ์ฐํ ๋ค์ด๋ก๋ ํ์ ์ฌ๋ถ
- self._discord_sent: bool = False # Discord ์๋ฆผ ๋ฐ์ก ์ฌ๋ถ
- # [Lazy Extraction] __init__์์๋ ๋ฌด๊ฑฐ์ด ๋ถ์์ ํ์ง ์์ต๋๋ค.
- # self.make_episode_info()
+ self.cookies_file: Optional[str] = None
+ self.need_special_downloader: bool = False
+ self._discord_sent: bool = False
+
+ # [Early Extraction] Parse basic metadata immediately so DB append has data
+ self.parse_metadata()
+
+ def parse_metadata(self) -> None:
+ """Extract basic info from title for early DB population."""
+ try:
+ title_full = self.info.get("title", "")
+ if not title_full:
+ return
+
+ match = re.compile(r"(?P
.*?)\s*((?P\d+)๊ธฐ)?\s*((?P\d+)ํ)").search(title_full)
+ if match:
+ self.content_title = match.group("title").strip()
+ if match.group("season"):
+ self.season = int(match.group("season"))
+ self.epi_queue = int(match.group("epi_no"))
+ else:
+ self.content_title = title_full
+ self.epi_queue = 1
+
+ # Predict initial filename/filepath for UI
+ epi_no = self.epi_queue
+ ret = "%s.S%sE%s.%s-OHNI24.mp4" % (
+ self.content_title,
+ "0%s" % self.season if self.season < 10 else self.season,
+ "0%s" % epi_no if epi_no < 10 else epi_no,
+ self.quality,
+ )
+ self.filename = Util.change_text_for_use_filename(ret)
+
+ # Savepath
+ self.savepath = P.ModelSetting.get("ohli24_download_path")
+ if P.ModelSetting.get_bool("ohli24_auto_make_folder"):
+ folder_name = self.content_title
+ if self.info.get("day", "").find("์๊ฒฐ") != -1:
+ folder_name = "%s %s" % (P.ModelSetting.get("ohli24_finished_insert"), self.content_title)
+ folder_name = Util.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"):
+ self.savepath = os.path.join(self.savepath, "Season %s" % int(self.season))
+
+ self.filepath = os.path.join(self.savepath, self.filename)
+ except Exception as e:
+ logger.error(f"Error in parse_metadata: {e}")
def refresh_status(self) -> None:
@@ -1700,43 +1740,21 @@ class Ohli24QueueEntity(AnimeQueueEntity):
# ------------------------------------------------------------------
# [METADATA PARSING] - Extract title, season, epi info first!
# ------------------------------------------------------------------
- # ๋ฉํ๋ฐ์ดํฐ๋ง ๋จผ์ ํ์ฑ (ํ์ผ๋ช
์์ฑ์ ํด์๋ ๊ฐ์ง ํ ์งํ)
- match = re.compile(r"(?P.*?)\s*((?P\d+)%s)?\s*((?P\d+)%s)" % ("๊ธฐ", "ํ")).search(
- self.info["title"]
- )
-
- epi_no = 1
- self.quality = "720P" # ๊ธฐ๋ณธ๊ฐ (ํด์๋ ๊ฐ์ง ์ ๋ฎ์ด์ฐ๊ธฐ)
-
- if match:
- self.content_title = match.group("title").strip()
- if "season" in match.groupdict() and match.group("season") is not None:
- self.season = int(match.group("season"))
-
- epi_no = int(match.group("epi_no"))
- else:
- self.content_title = self.info["title"]
- logger.debug("NOT MATCH")
-
- self.epi_queue = epi_no
- # NOTE: ํ์ผ๋ช
์ ํด์๋ ๊ฐ์ง ํ ์์ฑ (์๋ Step 2 ์ดํ)
-
- # Savepath ์์ฑ (filepath๋ ํ์ผ๋ช
์์ฑ ํ ์ค์ )
- self.savepath = P.ModelSetting.get("ohli24_download_path")
-
- if P.ModelSetting.get_bool("ohli24_auto_make_folder"):
- if self.info["day"].find("์๊ฒฐ") != -1:
- folder_name = "%s %s" % (
- P.ModelSetting.get("ohli24_finished_insert"),
- self.content_title,
- )
- else:
- folder_name = self.content_title
- folder_name = Util.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"):
- self.savepath = os.path.join(self.savepath, "Season %s" % int(self.season))
- # NOTE: self.filepath๋ ํ์ผ๋ช
์์ฑ ํ ์ค์ (Step 2 ์ดํ)
+ # [IMMEDIATE SYNC] Update DB with extracted metadata
+ try:
+ db_entity = ModelOhli24Item.get_by_ohli24_id(self.info["_id"])
+ if db_entity:
+ logger.debug(f"[SYNC] Syncing metadata for Ohli24 _id: {self.info.get('_id')}")
+ db_entity.title = self.content_title
+ db_entity.season = self.season
+ db_entity.episode_no = self.epi_queue
+ db_entity.savepath = self.savepath
+ db_entity.filename = self.filename
+ db_entity.filepath = self.filepath
+ db_entity.save()
+ except Exception as sync_err:
+ logger.error(f"Failed to sync metadata to DB: {sync_err}")
+
if not os.path.exists(self.savepath):
os.makedirs(self.savepath)
logger.info(f"self.savepath::> {self.savepath}")
@@ -1799,7 +1817,8 @@ class Ohli24QueueEntity(AnimeQueueEntity):
logger.info(f"Quality set from m3u8: {self.quality}")
# [FILENAME GENERATION] - ํด์๋ ๊ฐ์ง ํ ํ์ผ๋ช
์์ฑ
- if hasattr(self, 'epi_queue'):
+ # [FILENAME GENERATION] - Re-generate filename after quality detection
+ if self.epi_queue:
epi_no = self.epi_queue
ret = "%s.S%sE%s.%s-OHNI24.mp4" % (
self.content_title,
@@ -1811,7 +1830,6 @@ class Ohli24QueueEntity(AnimeQueueEntity):
self.filepath = os.path.join(self.savepath, self.filename)
# [NFD CHECK] Mac/Docker Compatibility
- # If NFC (Python standard) file doesn't exist, check NFD (Mac filesystem standard)
if not os.path.exists(self.filepath):
nfd_filename = unicodedata.normalize('NFD', self.filename)
nfd_filepath = os.path.join(self.savepath, nfd_filename)
@@ -1820,6 +1838,17 @@ class Ohli24QueueEntity(AnimeQueueEntity):
self.filename = nfd_filename
self.filepath = nfd_filepath
+ # [IMMEDIATE SYNC 2] Update filename/filepath after resolution detection
+ try:
+ db_entity = ModelOhli24Item.get_by_ohli24_id(self.info["_id"])
+ if db_entity:
+ db_entity.quality = self.quality
+ db_entity.filename = self.filename
+ db_entity.filepath = self.filepath
+ db_entity.save()
+ except:
+ pass
+
logger.info(f"self.filename::> {self.filename}")
if not video_url:
diff --git a/model_base.py b/model_base.py
index 864d7ca..f7dfe27 100644
--- a/model_base.py
+++ b/model_base.py
@@ -150,7 +150,13 @@ class AnimeQueueEntity(FfmpegQueueEntity):
# Anilife uses _id
if hasattr(model_class, 'get_by_anilife_id') and info.get('_id'):
db_entity = model_class.get_by_anilife_id(info['_id'])
- # Linkkf/Ohli24 might use different identifiers
+ # Ohli24 uses _id (via get_by_ohli24_id)
+ elif hasattr(model_class, 'get_by_ohli24_id') and info.get('_id'):
+ db_entity = model_class.get_by_ohli24_id(info['_id'])
+ # Linkkf uses _id (via get_by_linkkf_id)
+ elif hasattr(model_class, 'get_by_linkkf_id') and info.get('_id'):
+ db_entity = model_class.get_by_linkkf_id(info['_id'])
+ # Other modules might use get_by_id with db_id
elif hasattr(model_class, 'get_by_id') and info.get('db_id'):
db_entity = model_class.get_by_id(info['db_id'])
elif hasattr(model_class, 'get_by_content_code') and info.get('content_code'):
diff --git a/static/css/ohli24.css b/static/css/ohli24.css
index 9c99c85..95b1ade 100644
--- a/static/css/ohli24.css
+++ b/static/css/ohli24.css
@@ -1,7 +1,7 @@
/* Ohli24 Theme Variables & Overrides */
:root {
--slate-950: #0f172a;
- --slate-900: #0f172a; /* Same as 950 for deep background */
+ --slate-900: #0f172a;
--slate-800: #1e293b;
--slate-700: #334155;
--blue-500: #3b82f6;
@@ -23,17 +23,253 @@ body {
background-attachment: fixed !important;
}
-/* Ohli24 Specific Nav-Pills Overrides */
+/* General Layout Fixes */
+.container-fluid {
+ padding-left: 8px !important;
+ padding-right: 8px !important;
+ max-width: 100%;
+}
+
+/* Nav Pills Customization */
ul.nav.nav-pills.bg-light {
background-color: rgba(30, 41, 59, 0.6) !important;
- border: 1px solid rgba(255, 255, 255, 0.08) !important;
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 50rem !important;
+ padding: 6px !important;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
+ display: inline-flex !important;
+ flex-wrap: wrap;
+ justify-content: center;
+ width: auto !important;
+ margin-bottom: 20px;
}
ul.nav.nav-pills .nav-link {
color: #94a3b8 !important;
+ border-radius: 50rem !important;
+ padding: 8px 20px !important;
}
ul.nav.nav-pills .nav-link.active {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%) !important;
+ color: #fff !important;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4) !important;
}
+
+/* Search Bar Styling */
+.search-container {
+ background: rgba(30, 41, 59, 0.7);
+ backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 20px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ align-items: center;
+}
+
+.custom-select, .custom-input {
+ background-color: rgba(15, 23, 42, 0.6) !important;
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
+ color: #e2e8f0 !important;
+ border-radius: 8px !important;
+ height: 38px !important;
+}
+
+.custom-btn {
+ border-radius: 8px !important;
+ padding: 0 16px !important;
+ height: 38px !important;
+ font-weight: 600 !important;
+ border: none !important;
+}
+
+.btn-search { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: white !important; }
+.btn-reset { background: rgba(148, 163, 184, 0.1); color: #94a3b8 !important; }
+
+/* Episode Card (Mobile-First) - Scoped to List Page */
+.ohli24-list-page .episode-card {
+ display: flex;
+ flex-direction: column;
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.85) 100%);
+ border-radius: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.12);
+ margin-bottom: 12px;
+ transition: all 0.2s ease;
+ overflow: hidden;
+}
+
+.ohli24-list-page .episode-card-body {
+ display: flex;
+ flex-wrap: wrap; /* Key for mobile stacking */
+ padding: 12px;
+ width: 100%;
+ gap: 12px;
+}
+
+.ohli24-list-page .episode-thumb {
+ width: 60px;
+ height: 80px;
+ flex: 0 0 60px;
+ border-radius: 8px;
+ overflow: hidden;
+ background-color: #1e293b;
+ position: relative;
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3);
+}
+
+.ohli24-list-page .episode-thumb img { width: 100%; height: 100%; object-fit: cover; }
+
+.ohli24-list-page .episode-main-info {
+ flex: 1;
+ min-width: calc(100% - 84px); /* Force wrapping if needed, but allow sitting next to thumb */
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.ohli24-list-page .episode-title {
+ color: #f1f5f9;
+ font-weight: 700;
+ font-size: 15px;
+ margin-bottom: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.ohli24-list-page .episode-subtitle {
+ font-size: 11px;
+ color: #94a3b8;
+ margin-bottom: 6px;
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.ohli24-list-page .episode-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+ margin-bottom: 6px;
+}
+
+/* Episode Right Column / Actions - Wraps to 100% on mobile */
+.ohli24-list-page .episode-right-col {
+ flex: 0 0 100%; /* Full width on mobile */
+ margin-top: 4px;
+ padding-top: 12px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.ohli24-list-page .episode-actions {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 6px;
+}
+
+.ohli24-list-page .episode-actions .btn {
+ font-size: 12px !important;
+ padding: 8px 6px !important;
+ text-align: center;
+ border-radius: 6px !important;
+ white-space: nowrap;
+}
+
+/* Status Badges */
+.status-badge { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; display: inline-block; }
+.status-completed { background: rgba(16, 185, 129, 0.2); color: #34d399; }
+.status-wait { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
+
+/* Desktop Adaptations - Scoped to List Page */
+@media (min-width: 768px) {
+ .ohli24-list-page .episode-card { margin-bottom: 10px; }
+
+ .ohli24-list-page .episode-card-body {
+ flex-direction: row;
+ align-items: center;
+ padding: 10px 20px;
+ gap: 20px;
+ }
+
+ .ohli24-list-page .episode-top-info {
+ flex: 1;
+ align-items: center;
+ min-width: 0;
+ }
+
+ .ohli24-list-page .episode-thumb { width: 60px; height: 80px; }
+
+ .ohli24-list-page .episode-main-info { justify-content: center; }
+
+ .ohli24-list-page .episode-title { font-size: 16px; margin-bottom: 2px; }
+
+ .ohli24-list-page .file-path { margin-top: 2px; padding: 4px 8px; display: inline-block; max-width: 100%; }
+
+ .ohli24-list-page .episode-right-col {
+ width: auto;
+ margin-top: 0;
+ padding-top: 0;
+ border-top: none;
+ padding-left: 20px;
+ display: flex;
+ align-items: center;
+ min-width: 320px;
+ }
+
+ .ohli24-list-page .episode-actions { display: flex; flex-wrap: nowrap; gap: 6px; }
+ .ohli24-list-page .episode-actions .btn { flex: none; width: auto; min-width: 80px; font-size: 11px !important; }
+}
+
+/* Queue Specific Styles - Scoped to Queue Page */
+.ohli24-queue-page .queue-header {
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.9) 0%, rgba(15, 23, 42, 0.95) 100%);
+ border-radius: 12px;
+ padding: 15px 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.ohli24-queue-page .queue-item {
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.85) 100%);
+ border-radius: 10px;
+ border: 1px solid rgba(148, 163, 184, 0.12);
+ padding: 15px;
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.ohli24-queue-page .progress-wrapper {
+ position: relative;
+ height: 32px;
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 16px;
+ overflow: hidden;
+ width: 300px;
+}
+
+.ohli24-queue-page .progress-bar { height: 100%; transition: width 0.3s ease; }
+.ohli24-queue-page .status-waiting { background: linear-gradient(90deg, #94a3b8, #64748b); }
+.ohli24-queue-page .status-downloading { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
+.ohli24-queue-page .status-completed-bar { background: linear-gradient(90deg, #22c55e, #4ade80); }
+
+/* Common Modal Fixes */
+.modal-content {
+ background: rgba(15, 23, 42, 0.95) !important;
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
+ border-radius: 12px !important;
+}
+
+@media (max-width: 768px) {
+ .ohli24-queue-page .progress-wrapper { width: 100%; }
+ .ohli24-queue-page .queue-item { flex-direction: column; align-items: stretch; }
+ .ohli24-queue-page ul.nav.nav-pills.bg-light { margin-top: 40px !important; }
+}
diff --git a/templates/anime_downloader_ohli24_list.html b/templates/anime_downloader_ohli24_list.html
index 38cf9a3..b7d4d94 100644
--- a/templates/anime_downloader_ohli24_list.html
+++ b/templates/anime_downloader_ohli24_list.html
@@ -4,7 +4,7 @@
-
@@ -513,513 +511,27 @@
- /* General Layout Tweaks */
- .container-fluid {
- padding-left: 4px !important;
- padding-right: 4px !important;
- max-width: 100%;
- margin: 0 auto;
- }
-
- .content-cloak {
- padding-top: 10px;
- }
-
- /* Search Bar Design */
- .search-container {
- background: rgba(30, 41, 59, 0.7);
- backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 12px;
- padding: 10px;
- margin-bottom: 20px;
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- align-items: center;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- }
-
- .search-group-left {
- display: flex;
- gap: 8px;
- flex: 0 0 auto;
- }
-
- .search-group-right {
- display: flex;
- gap: 8px;
- flex: 1;
- }
-
- /* Custom Form Controls */
- .custom-select, .custom-input {
- background-color: rgba(15, 23, 42, 0.6) !important;
- border: 1px solid rgba(148, 163, 184, 0.2) !important;
- color: #e2e8f0 !important;
- border-radius: 8px !important;
- padding: 6px 12px !important;
- height: 38px !important;
- font-size: 13px !important;
- transition: all 0.2s;
- }
-
- .custom-select:focus, .custom-input:focus {
- border-color: #3b82f6 !important;
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2) !important;
- outline: none;
- }
-
- .custom-btn {
- border-radius: 8px !important;
- padding: 0 16px !important;
- height: 38px !important;
- font-weight: 600 !important;
- display: flex;
- align-items: center;
- gap: 6px;
- transition: all 0.2s;
- border: none !important;
- }
-
- .btn-search {
- background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
- color: white !important;
- }
- .btn-search:hover { box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); transform: translateY(-1px); }
-
- .btn-reset {
- background: rgba(148, 163, 184, 0.1);
- color: #94a3b8 !important;
- }
- .btn-reset:hover { background: rgba(148, 163, 184, 0.2); color: white !important; }
-
- /* List Container - List View */
- #list_div {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 0 5px;
- width: 100%;
- }
-
- /* Episode Card Style */
- .episode-card {
- display: flex;
- flex-direction: column;
- background: linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(15, 23, 42, 0.85) 100%);
- border-radius: 12px;
- border: 1px solid rgba(148, 163, 184, 0.12);
- overflow: hidden;
- transition: all 0.2s ease;
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
- position: relative;
- }
-
- .episode-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
- border-color: rgba(96, 165, 250, 0.4);
- background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.95) 100%);
- z-index: 10;
- }
-
- .episode-card-body {
- padding: 10px;
- display: flex;
- flex-direction: row; /* Mobile Default: Row for proper structure */
- flex-wrap: wrap; /* Allow wrapping on small mobile */
- width: 100%;
- }
-
- /* Inner Components */
- .episode-thumb {
- width: 60px;
- height: 80px;
- flex-shrink: 0;
- margin-right: 12px;
- border-radius: 8px;
- overflow: hidden;
- background-color: #1e293b;
- }
-
- .episode-thumb img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
-
- .episode-badge {
- position: absolute;
- top: 5px;
- left: 5px;
- background: #facc15; /* Amber/Yellow 400 */
- color: #000;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 11px;
- font-weight: 800;
- z-index: 5;
- box-shadow: 0 2px 4px rgba(0,0,0,0.3);
- border: 1px solid rgba(0,0,0,0.1);
- pointer-events: none;
- text-transform: uppercase;
- }
-
- .episode-main-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
-
- .episode-title {
- color: #f1f5f9;
- font-weight: 600;
- font-size: 14px;
- line-height: 1.4;
- margin-bottom: 2px;
- word-break: break-all;
- }
-
- .episode-subtitle {
- font-size: 12px;
- color: #94a3b8;
- margin-bottom: 4px;
- line-height: 1.3;
- display: block;
- }
-
- .episode-meta {
- font-size: 11px;
- color: #94a3b8;
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- align-items: center;
- margin-bottom: 4px;
- }
-
- .meta-tag {
- background: rgba(148, 163, 184, 0.1);
- padding: 1px 5px;
- border-radius: 4px;
- color: #cbd5e1;
- }
-
- .status-badge {
- font-size: 10px;
- font-weight: 700;
- padding: 2px 5px;
- border-radius: 4px;
- text-transform: uppercase;
- min-width: 60px;
- text-align: center;
- }
- .status-completed { background: rgba(16, 185, 129, 0.2); color: #34d399; }
- .status-wait { background: rgba(245, 158, 11, 0.2); color: #fbbf24; }
- .status-downloading { background: rgba(59, 130, 246, 0.2); color: #60a5fa; }
- .status-failed { background: rgba(239, 68, 68, 0.2); color: #f87171; }
-
- .file-path {
- font-size: 11px;
- color: #64748b;
- word-break: break-all;
- margin-top: 2px;
- display: block;
- }
-
- /* Right Column (Mobile) */
- .episode-right-col {
- width: 100%;
- margin-top: 10px;
- padding-top: 10px;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .date-info {
- font-size: 11px;
- color: #94a3b8;
- display: flex;
- gap: 10px;
- }
-
- .episode-actions {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 5px;
- }
-
- .episode-actions .btn {
- font-size: 11px !important;
- padding: 4px 8px !important;
- white-space: nowrap;
- width: 100%;
- }
-
- /* Desktop List View Transformation */
- @media (min-width: 768px) {
- .episode-card {
- flex-direction: row;
- align-items: center;
- padding: 10px;
- min-height: 80px;
- }
-
- .episode-card-body {
- flex-wrap: nowrap;
- align-items: stretch;
- padding: 0;
- width: 100%;
- }
-
- .episode-thumb {
- margin-right: 15px;
- margin-bottom: 0;
- }
-
- .episode-main-info {
- margin-bottom: 0;
- justify-content: center;
- }
-
- .episode-title {
- font-size: 15px;
- word-break: keep-all;
- }
-
- .episode-right-col {
- width: auto;
- margin-top: 0;
- padding-top: 0;
- border-top: none;
- margin-left: auto; /* Push to right */
- align-items: flex-end;
- justify-content: center;
- min-width: 220px;
- }
-
- .date-info {
- justify-content: flex-end;
- margin-bottom: 5px;
- order: -1;
- }
-
- .episode-actions {
- display: flex;
- flex-direction: row;
- gap: 5px;
- }
-
- .episode-actions .btn {
- width: auto;
- }
-
- .episode-thumb img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- }
-
- /* Hide Original Headers */
- .show-grid { margin-bottom: 20px; }
- .container-fluid > div:nth-child(3), /* Head Top HR */
- .container-fluid > div:nth-child(4), /* Headers Row */
- .container-fluid > div:nth-child(5) /* Head Bottom HR */
- {
- display: none !important;
- }
-
-
-
{% endblock %}
\ No newline at end of file
diff --git a/templates/anime_downloader_ohli24_queue.html b/templates/anime_downloader_ohli24_queue.html
index 5e993b4..9b90cac 100644
--- a/templates/anime_downloader_ohli24_queue.html
+++ b/templates/anime_downloader_ohli24_queue.html
@@ -4,7 +4,7 @@
-