diff --git a/README.md b/README.md index 0e6af27..e50097e 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,24 @@ ## ๐Ÿ“ ๋ณ€๊ฒฝ ์ด๋ ฅ (Changelog) +### v0.4.0 (2026-01-02) +- **Discord ์•Œ๋ฆผ ๊ฐœ์„ **: + - ๋‹ค์šด๋กœ๋“œ ์™„๋ฃŒ ์‹œ์—๋งŒ ์•Œ๋ฆผ ์ „์†ก (์‹œ์ž‘ ์‹œ ์•Œ๋ฆผ ์ œ๊ฑฐ) + - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€์— ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ๋ฐ ํŒŒ์ผ๋ช… ํฌํ•จ +- **DB ๋งคํ•‘ ๊ฐœ์„ **: + - ๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘ ์ฆ‰์‹œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ œ๋ชฉ, ์—ํ”ผ์†Œ๋“œ ๋ฒˆํ˜ธ, ํ™”์งˆ ๋“ฑ) DB ๋™๊ธฐํ™” + - `download_completed`์—์„œ ๋ชจ๋“  ํ•„๋“œ ์ •ํ™•ํžˆ ๋งคํ•‘ +- **UI/UX ๊ฐœ์„ **: + - Ohli24 ๋ชฉ๋ก์— ์—ํ”ผ์†Œ๋“œ ๋ฒˆํ˜ธ ๋ฐฐ์ง€ ์ถ”๊ฐ€ (๊ณ ๋Œ€๋น„ ๋…ธ๋ž€์ƒ‰) + - Linkkf ๋ชฉ๋ก์— **"์ž๋ง‰ํ•ฉ์นจ"** ๋ฒ„ํŠผ ์ถ”๊ฐ€ (ffmpeg๋กœ SRT ์ž๋ง‰ MP4์— ์‚ฝ์ž…) +- **Linkkf ๋‹ค์šด๋กœ๋“œ ์ˆ˜์ •**: + - `get_downloader` ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๋ฐ ์„ค์ • ํŽ˜์ด์ง€์˜ ๋‹ค์šด๋กœ๋“œ ๋ฐฉ์‹ ๋ฐ˜์˜ + - `prepare_extra` URL ๋ฎ์–ด์“ฐ๊ธฐ ๋ฒ„๊ทธ ์ˆ˜์ • + - yt-dlp Fragment ํŒŒ์ผ ์ž๋™ ์ •๋ฆฌ +- **๋กœ๊ทธ ์ตœ์ ํ™”**: + - yt-dlp ์ง„ํ–‰๋ฅ  ๋กœ๊ทธ ๋นˆ๋„ ๊ฐ์†Œ (10ํšŒ๋‹น 1ํšŒ) + - ์ค‘๋ณต ๋กœ๊ทธ ์ œ๊ฑฐ (`download_completed` ๋‹จ์ผ ํ˜ธ์ถœ) + ### v0.3.7 (2026-01-01) - **์„ค์ • ํŽ˜์ด์ง€ ํด๋” ํƒ์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€**: - Ohli24, Anilife, Linkkf ๋ชจ๋“  ์„ค์ • ํŽ˜์ด์ง€์— **ํด๋” ํƒ์ƒ‰ ๋ฒ„ํŠผ** ์ ์šฉ @@ -91,20 +109,6 @@ - ๊ฒ€์ƒ‰์ฐฝ ๋ฐ ๋ฒ„ํŠผ UI ๋””์ž์ธ ๊ฐœ์„  (๋†’์ด ์กฐ์ •, ์ •๋ ฌ ์ˆ˜์ •, "Elegant" ์Šคํƒ€์ผ ์ ์šฉ) - "Top" ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋‚ด๋ถ€ API ์—ฐ๋™์œผ๋กœ ์ „ํ™˜ํ•˜์—ฌ ์ •ํ™•๋„ ํ–ฅ์ƒ -### v0.4.0 (2025-01-01) -- **UI/UX ๋Œ€๊ทœ๋ชจ ๊ฐœํŽธ**: - - ์ „๋ฐ˜์ ์ธ ๋””์ž์ธ์„ **"Midnight Forest"** ํ…Œ๋งˆ๋กœ ํ†ต์ผ (์ง™์€ ๋…น์ƒ‰/์ง™์€ ์ฒญ์ƒ‰ ๋ฒ ์ด์Šค) - - Linkkf ๋ฐ Ohli24 ๋ชฉ๋ก ํŽ˜์ด์ง€์— Animate.css ๊ธฐ๋ฐ˜์˜ **Custom Delete Modal** ์ ์šฉ (๊ธฐ์กด native confirm ํŒ์—… ๋Œ€์ฒด) - - ํŽ˜์ด์ง€ ์ขŒ์šฐ ์—ฌ๋ฐฑ์„ 5px๋กœ ์ถ•์†Œํ•˜์—ฌ ๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํƒ‘ ๋ชจ๋‘์—์„œ ์ปดํŒฉํŠธํ•œ ๋ ˆ์ด์•„์›ƒ ์ œ๊ณต - - Linkkf ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€์— ์—ํ”ผ์†Œ๋“œ ๋„˜๋ฒ„ ๋ฐฐ์ง€ ์ถ”๊ฐ€ -- **๊ธฐ๋Šฅ ๊ฐœ์„ **: - - **Queue ๊ด€๋ฆฌ ๊ฐ•ํ™”**: ํ ์ดˆ๊ธฐํ™”(Reset) ๋ฐ ์™„๋ฃŒ๋œ ํ•ญ๋ชฉ ์‚ญ์ œ(Delete Completed) ๋ฒ„ํŠผ ์ถ”๊ฐ€ - - **์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์ตœ์ ํ™”**: ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์‹คํŒจ ์‹œ ํšจ์œจ์ ์ธ Fallback ์ฒ˜๋ฆฌ ์ ์šฉ (placeholder ์„œ๋น„์Šค ์—ฐ๋™) - - **ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ฒ„๊ทธ ์ˆ˜์ •**: Linkkf ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๋ฐœ์ƒํ•˜๋˜ undefined ํŽ˜์ด์ง€ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ -- **์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ**: - - ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ DB ์ž‘์—… ์‹œ `app_context` ์˜ค๋ฅ˜ ์ˆ˜์ • - - `yt-dlp` ๋‹ค์šด๋กœ๋“œ ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ ๊ฐœ์„  (์ข€๋น„ ํ”„๋กœ์„ธ์Šค ๋ฐฉ์ง€ ๋ฐ ํ™•์‹คํ•œ ์ทจ์†Œ ์ฒ˜๋ฆฌ) - ### v0.3.0 (2025-12-31) - **VideoJS ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ**: ๋น„๋””์˜ค ํ”Œ๋ ˆ์ด์–ด์—์„œ ๋‹ค์Œ ์—ํ”ผ์†Œ๋“œ ์ž๋™ ์žฌ์ƒ - **ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ UI**: ์ด์ „/๋‹ค์Œ ๋ฒ„ํŠผ, ์—ํ”ผ์†Œ๋“œ ๋ชฉ๋ก ํ† ๊ธ€ diff --git a/info.yaml b/info.yaml index a72c2b0..65604b2 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "์• ๋‹ˆ ๋‹ค์šด๋กœ๋”" -version: "0.3.9" +version: "0.4.1" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/lib/ytdlp_downloader.py b/lib/ytdlp_downloader.py index 46c5887..8720e13 100644 --- a/lib/ytdlp_downloader.py +++ b/lib/ytdlp_downloader.py @@ -357,9 +357,13 @@ class YtdlpDownloader: logger.warning(f"yt-dlp output notice: {line}") self.error_output.append(line) - # Aria2c / ๋ณ‘๋ ฌ ๋‹ค์šด๋กœ๋“œ ๋กœ๊ทธ ๋กœ๊น… + # Aria2c / ๋ณ‘๋ ฌ ๋‹ค์šด๋กœ๋“œ ๋กœ๊ทธ - 10ํšŒ๋‹น 1ํšŒ๋งŒ ๋กœ๊น… (๋กœ๊ทธ ๋ถ€ํ•˜ ๊ฐ์†Œ) if 'aria2c' in line.lower() or 'fragment' in line.lower(): - logger.info(f"yt-dlp: {line}") + if not hasattr(self, '_fragment_log_count'): + self._fragment_log_count = 0 + self._fragment_log_count += 1 + if self._fragment_log_count % 10 == 1: # 1, 11, 21, 31... ๋ฒˆ์งธ๋งŒ ๋กœ๊น… + logger.debug(f"yt-dlp: {line}") self.process.wait() @@ -374,6 +378,36 @@ class YtdlpDownloader: os.remove(self.output_path) return False, f"CDN ๋ณด์•ˆ ์ฐจ๋‹จ(๊ฐ€์งœ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ๋จ: {file_size}B)" except: pass + + # [Fragment Cleanup] yt-dlp ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ (Frag*, .ytdl, .part ๋“ฑ) + try: + import glob + dirname = os.path.dirname(self.output_path) + basename = os.path.basename(self.output_path) + name_without_ext = os.path.splitext(basename)[0] + + # ํŒจํ„ด ๋ชฉ๋ก: *-Frag*, .ytdl, .part ํŒŒ์ผ๋“ค + cleanup_patterns = [ + os.path.join(dirname, f"{name_without_ext}*-Frag*"), + os.path.join(dirname, f"{name_without_ext}*.ytdl"), + os.path.join(dirname, f"{name_without_ext}*.part"), + os.path.join(dirname, "*-Frag*"), # ์ผ๋ฐ˜ Fragment ํŒŒ์ผ + ] + + cleaned_count = 0 + for pattern in cleanup_patterns: + for frag_file in glob.glob(pattern): + try: + os.remove(frag_file) + cleaned_count += 1 + except: + pass + + if cleaned_count > 0: + logger.info(f"[Cleanup] Removed {cleaned_count} temporary fragment files") + except Exception as cleanup_err: + logger.debug(f"Fragment cleanup error (non-critical): {cleanup_err}") + return True, "Download completed" error_msg = "\n".join(self.error_output[-3:]) if self.error_output else f"Exit code {self.process.returncode}" diff --git a/mod_linkkf.py b/mod_linkkf.py index 9101da1..d43f9c0 100644 --- a/mod_linkkf.py +++ b/mod_linkkf.py @@ -250,6 +250,77 @@ class LogicLinkkf(AnimeModuleBase): return jsonify({"ret": "error", "log": "No ID provided"}) return jsonify(ModelLinkkfItem.delete_by_id(db_id)) + elif sub == "merge_subtitle": + # ์ž๋ง‰ ํ•ฉ์น˜๊ธฐ - ffmpeg๋กœ SRT๋ฅผ MP4์— ์‚ฝ์ž… + import subprocess + import shutil + + db_id = request.form.get("id") + if not db_id: + return jsonify({"ret": "error", "message": "No ID provided"}) + + try: + db_item = ModelLinkkfItem.get_by_id(int(db_id)) + if not db_item: + return jsonify({"ret": "error", "message": "Item not found"}) + + mp4_path = db_item.filepath + if not mp4_path or not os.path.exists(mp4_path): + return jsonify({"ret": "error", "message": f"MP4 file not found: {mp4_path}"}) + + # SRT ํŒŒ์ผ ๊ฒฝ๋กœ (MP4์™€ ๋™์ผ ๊ฒฝ๋กœ์— .srt ํ™•์žฅ์ž) + srt_path = os.path.splitext(mp4_path)[0] + ".srt" + if not os.path.exists(srt_path): + return jsonify({"ret": "error", "message": f"SRT file not found: {srt_path}"}) + + # ์ถœ๋ ฅ ํŒŒ์ผ: *_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 ๋ช…๋ น์–ด: ์ž๋ง‰์„ soft embed (mov_text ์ฝ”๋ฑ) + # -i mp4 -i srt -c:v copy -c:a copy -c:s mov_text output + 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=300) + + if result.returncode != 0: + logger.error(f"ffmpeg error: {result.stderr}") + return jsonify({"ret": "error", "message": f"ffmpeg failed: {result.stderr[-200:]}"}) + + if not os.path.exists(output_path): + return jsonify({"ret": "error", "message": "Output file was not created"}) + + output_size = os.path.getsize(output_path) + logger.info(f"[Merge Subtitle] Created: {output_path} ({output_size} bytes)") + + return jsonify({ + "ret": "success", + "message": f"์ž๋ง‰ ํ•ฉ์นจ ์™„๋ฃŒ!", + "output_file": os.path.basename(output_path), + "output_size": output_size + }) + + except subprocess.TimeoutExpired: + return jsonify({"ret": "error", "message": "ffmpeg timeout (5๋ถ„ ์ดˆ๊ณผ)"}) + except Exception as e: + logger.error(f"merge_subtitle error: {e}") + logger.error(traceback.format_exc()) + return jsonify({"ret": "error", "message": str(e)}) + elif sub == "get_playlist": # ํ˜„์žฌ ํŒŒ์ผ๊ณผ ๊ฐ™์€ ํด๋”์—์„œ ๋‹ค์Œ ์—ํ”ผ์†Œ๋“œ๋“ค ์ฐพ๊ธฐ try: @@ -1460,9 +1531,8 @@ class LogicLinkkf(AnimeModuleBase): return "queue_exist" else: db_entity = ModelLinkkfItem.get_by_linkkf_id(episode_info["_id"]) - logger.info(f"db_entity: {db_entity}") - - logger.debug("db_entity:::> %s", db_entity) + # logger.info(f"db_entity: {db_entity}") + # logger.debug("db_entity:::> %s", db_entity) # logger.debug("db_entity.status ::: %s", db_entity.status) if db_entity is None: entity = LinkkfQueueEntity(P, self, episode_info) @@ -1614,6 +1684,27 @@ class LinkkfQueueEntity(FfmpegQueueEntity): self.playid_url = playid_url self.url = playid_url # ์ดˆ๊ธฐ๊ฐ’ ์„ค์ • + def get_downloader(self, video_url, output_file, callback=None, callback_function=None): + """ + Factory๋ฅผ ํ†ตํ•ด ๋‹ค์šด๋กœ๋” ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + ์„ค์ •์—์„œ ๋‹ค์šด๋กœ๋“œ ๋ฐฉ์‹์„ ์ฝ์–ด์˜ต๋‹ˆ๋‹ค. + """ + 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}") + + return DownloaderFactory.get_downloader( + method=method, + video_url=video_url, + output_file=output_file, + headers=self.headers, + callback=callback, + callback_id="linkkf", + callback_function=callback_function + ) + def prepare_extra(self): """ [Lazy Extraction] @@ -1639,7 +1730,37 @@ class LinkkfQueueEntity(FfmpegQueueEntity): else: # ์ถ”์ถœ ์‹คํŒจ ์‹œ ์›๋ณธ URL ์‚ฌ์šฉ (fallback) self.url = self.playid_url - logger.warning(f"Failed to extract video URL, using playid URL: {self.playid_url}") + + # ------------------------------------------------------------------ + # [IMMEDIATE SYNC] - Update DB with all extracted metadata + # ------------------------------------------------------------------ + try: + from .mod_linkkf import ModelLinkkfItem + db_item = ModelLinkkfItem.get_by_linkkf_id(self.info.get("_id")) + if db_item: + logger.debug(f"[SYNC] Syncing metadata for Linkkf _id: {self.info.get('_id')}") + # Parse episode number if possible for DB field + epi_no = None + try: + match = re.search(r"(?P\d+)", str(self.info.get("ep_num", ""))) + if match: + epi_no = int(match.group("epi_no")) + except: + pass + + db_item.title = self.content_title + db_item.season = int(self.season) if self.season else 1 + db_item.episode_no = epi_no + db_item.quality = self.quality + db_item.savepath = self.savepath + db_item.filename = self.filename + db_item.filepath = self.filepath + db_item.video_url = self.url + db_item.vtt_url = self.vtt + db_item.save() + except Exception as sync_err: + logger.error(f"[SYNC] Failed to sync Linkkf metadata in prepare_extra: {sync_err}") + except Exception as e: logger.error(f"Exception in video URL extraction: {e}") logger.error(traceback.format_exc()) @@ -1659,6 +1780,10 @@ class LinkkfQueueEntity(FfmpegQueueEntity): db_item.completed_time = datetime.now() db_item.filepath = self.filepath db_item.filename = self.filename + db_item.savepath = self.savepath + db_item.quality = self.quality + db_item.video_url = self.url + db_item.vtt_url = self.vtt db_item.save() logger.info(f"Updated DB status to 'completed' for episode {db_item.id}") else: diff --git a/mod_ohli24.py b/mod_ohli24.py index eeddb93..ba94fab 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -1553,19 +1553,6 @@ class Ohli24QueueEntity(AnimeQueueEntity): self.module_logic.socketio_callback("status", self.as_dict()) - # Discord Notification Trigger (All downloaders) - try: - if getattr(self, 'ffmpeg_status', 0) == 5: # DOWNLOADING - if not getattr(self, '_discord_sent', False): - self._discord_sent = True - title = self.info.get('title', 'Unknown Title') - filename = getattr(self, 'filename', 'Unknown File') - # ์ธ๋„ค์ผ ์ด๋ฏธ์ง€ - image_link ๋˜๋Š” thumbnail ํ•„๋“œ์—์„œ ๊ฐ€์ ธ์˜ด - poster_url = self.info.get('image_link', '') or self.info.get('thumbnail', '') - logger.debug(f"Discord poster_url: {poster_url}") - self.module_logic.send_discord_notification("๋‹ค์šด๋กœ๋“œ ์‹œ์ž‘", title, filename, poster_url) - except Exception as e: - logger.error(f"Failed to check/send discord notification in refresh_status: {e}") # ์ถ”๊ฐ€: /queue ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋กœ๋„ ๋ช…์‹œ์ ์œผ๋กœ ์ „์†ก try: from framework import socketio @@ -1803,6 +1790,26 @@ class Ohli24QueueEntity(AnimeQueueEntity): except Exception as srt_err: logger.warning(f"Subtitle download failed: {srt_err}") + # ------------------------------------------------------------------ + # [IMMEDIATE SYNC] - Update DB with all extracted metadata + # ------------------------------------------------------------------ + try: + db_entity = ModelOhli24Item.get_by_ohli24_id(self.info["_id"]) + if db_entity: + logger.debug(f"[SYNC] Syncing metadata for _id: {self.info['_id']}") + db_entity.title = self.content_title + db_entity.season = self.season + db_entity.episode_no = self.epi_queue + db_entity.quality = self.quality + db_entity.savepath = self.savepath + db_entity.filename = self.filename + db_entity.filepath = self.filepath + db_entity.video_url = self.url + db_entity.vtt_url = self.srt_url or self.vtt + db_entity.save() + except Exception as sync_err: + logger.error(f"[SYNC] Failed to sync metadata in prepare_extra: {sync_err}") + except Exception as e: P.logger.error("Exception:%s", e) P.logger.error(traceback.format_exc()) diff --git a/templates/anime_downloader_linkkf_list.html b/templates/anime_downloader_linkkf_list.html index f6a253d..b146d0c 100644 --- a/templates/anime_downloader_linkkf_list.html +++ b/templates/anime_downloader_linkkf_list.html @@ -765,6 +765,7 @@ $(document).ready(function(){ str += '
'; if (item.status === 'completed') { str += ''; + str += ''; } str += ''; str += ''; @@ -808,6 +809,42 @@ $(document).ready(function(){ } }); }); + + // ์ž๋ง‰ ํ•ฉ์นจ ๋ฒ„ํŠผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ + $(document).on('click', '.btn-merge-sub', function() { + var $btn = $(this); + var itemId = $btn.data('id'); + var filename = $btn.data('filename'); + + // ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ + $btn.prop('disabled', true).html(' ์ฒ˜๋ฆฌ์ค‘...'); + + $.ajax({ + url: '/' + package_name + '/ajax/' + sub + '/merge_subtitle', + type: 'POST', + data: { id: itemId }, + dataType: 'json', + success: function(data) { + if (data.ret === 'success') { + $.notify(' ' + data.message + '
' + data.output_file + '', { + type: 'success', + delay: 5000 + }); + } else { + $.notify(' ์‹คํŒจ: ' + data.message, { + type: 'danger', + delay: 8000 + }); + } + }, + error: function(xhr, status, error) { + $.notify('์š”์ฒญ ์‹คํŒจ: ' + error, { type: 'danger' }); + }, + complete: function() { + $btn.prop('disabled', false).html(' ์ž๋ง‰ํ•ฉ์นจ'); + } + }); + }); {% endblock %} \ No newline at end of file diff --git a/templates/anime_downloader_ohli24_list.html b/templates/anime_downloader_ohli24_list.html index faf9258..e39eca5 100644 --- a/templates/anime_downloader_ohli24_list.html +++ b/templates/anime_downloader_ohli24_list.html @@ -468,10 +468,15 @@ // Thumbnail let imgHtml = ''; + let badgeHtml = ''; + if (item.episode_no) { + badgeHtml = `
${item.episode_no}ํ™”
`; + } + if (item.thumbnail) { - imgHtml = ``; + imgHtml = `${badgeHtml}`; } else { - imgHtml = ``; + imgHtml = `${badgeHtml}`; } // Date @@ -771,6 +776,23 @@ 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;