Spaces:
Running
Running
Hermes Agent commited on
Commit ·
42399d1
1
Parent(s): 1b2d8f2
fix(apex): eliminate ghost tabs, deduplicate media, enforce uniform 2:3 continue watching cards, fix detail page title wrap, and pre-warm streamer to prevent source errors
Browse files- apex/combined-entrypoint.sh +13 -3
- apex/repair_jellyfin_libraries.py +111 -0
- apex/tests/test_repair_jellyfin_libraries.py +54 -0
- apex/ui/abyss.css +73 -38
- apex/ui/spotlight-loader.js +1 -1
apex/combined-entrypoint.sh
CHANGED
|
@@ -337,6 +337,19 @@ cp -f "$ENCODING_XML" "/data/jellyfin/config/encoding.xml" 2>/dev/null || true
|
|
| 337 |
sed -i 's|<EnableVideoEncoding>false</EnableVideoEncoding>|<EnableVideoEncoding>true</EnableVideoEncoding>|g' "$ENCODING_XML" "/data/jellyfin/config/encoding.xml" 2>/dev/null || true
|
| 338 |
echo "[JELLYFIN] Configured high-performance video encoding & direct stream remux in $ENCODING_XML"
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
echo "[HEALTH] Jellyfin starting in background (local datadir)..."
|
| 341 |
launch_jellyfin
|
| 342 |
|
|
@@ -348,9 +361,6 @@ fi
|
|
| 348 |
sync_jellyfin_back &
|
| 349 |
SYNC_PID=$!
|
| 350 |
|
| 351 |
-
echo "[HEALTH] Telegram streamer starting in background..."
|
| 352 |
-
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 python3 /opt/apex/tg_streamer.py &
|
| 353 |
-
TG_STREAMER_PID=$!
|
| 354 |
|
| 355 |
echo "[HEALTH] FastAPI Gateway (PID $FASTAPI_PID) and Nginx (PID $NGINX_PID) active."
|
| 356 |
|
|
|
|
| 337 |
sed -i 's|<EnableVideoEncoding>false</EnableVideoEncoding>|<EnableVideoEncoding>true</EnableVideoEncoding>|g' "$ENCODING_XML" "/data/jellyfin/config/encoding.xml" 2>/dev/null || true
|
| 338 |
echo "[JELLYFIN] Configured high-performance video encoding & direct stream remux in $ENCODING_XML"
|
| 339 |
|
| 340 |
+
echo "[HEALTH] Telegram streamer starting in background..."
|
| 341 |
+
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 python3 /opt/apex/tg_streamer.py &
|
| 342 |
+
TG_STREAMER_PID=$!
|
| 343 |
+
|
| 344 |
+
echo "[HEALTH] Awaiting Telegram streamer initialization on port 8082..."
|
| 345 |
+
for i in $(seq 1 40); do
|
| 346 |
+
if curl -s -f http://127.0.0.1:8082/health >/dev/null 2>&1; then
|
| 347 |
+
echo "[HEALTH] Telegram streamer is active and healthy on port 8082 (took ${i}s)."
|
| 348 |
+
break
|
| 349 |
+
fi
|
| 350 |
+
sleep 1
|
| 351 |
+
done
|
| 352 |
+
|
| 353 |
echo "[HEALTH] Jellyfin starting in background (local datadir)..."
|
| 354 |
launch_jellyfin
|
| 355 |
|
|
|
|
| 361 |
sync_jellyfin_back &
|
| 362 |
SYNC_PID=$!
|
| 363 |
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
echo "[HEALTH] FastAPI Gateway (PID $FASTAPI_PID) and Nginx (PID $NGINX_PID) active."
|
| 366 |
|
apex/repair_jellyfin_libraries.py
CHANGED
|
@@ -850,6 +850,116 @@ def heal_missing_db_strm_paths(databases=None):
|
|
| 850 |
except Exception as e:
|
| 851 |
print(f"[REPAIR] Note healing missing DB STRMs: {e}")
|
| 852 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
def main():
|
| 854 |
env_force = os.environ.get("RESET_JELLYFIN_PASSWORD", "").strip().lower() in ("1", "true", "yes")
|
| 855 |
manual_trigger = any(os.path.exists(p) for p in MANUAL_RESET_TRIGGERS)
|
|
@@ -880,6 +990,7 @@ def main():
|
|
| 880 |
heal_mismatched_media()
|
| 881 |
heal_channel_and_db_names()
|
| 882 |
heal_missing_db_strm_paths()
|
|
|
|
| 883 |
repair_user_subtitle_preferences()
|
| 884 |
repair_user_policy_preferences()
|
| 885 |
repair_system_xml()
|
|
|
|
| 850 |
except Exception as e:
|
| 851 |
print(f"[REPAIR] Note healing missing DB STRMs: {e}")
|
| 852 |
|
| 853 |
+
|
| 854 |
+
def deduplicate_media_strms(roots=None, databases=None):
|
| 855 |
+
"""
|
| 856 |
+
Deduplicates .strm files on disk and removes duplicate rows from Jellyfin BaseItems.
|
| 857 |
+
When multiple folders or .strm files point to the exact same stream URL, picks the
|
| 858 |
+
canonical version, deletes the duplicate folder/file from disk, and removes the redundant
|
| 859 |
+
rows from BaseItems in jellyfin.db.
|
| 860 |
+
"""
|
| 861 |
+
media_roots = roots or [
|
| 862 |
+
"/data/jellyfin/media/Movies",
|
| 863 |
+
"/data/jellyfin/media/TV Shows",
|
| 864 |
+
"/opt/jellyfin-local/media/Movies",
|
| 865 |
+
"/opt/jellyfin-local/media/TV Shows",
|
| 866 |
+
]
|
| 867 |
+
db_list = databases or TARGET_DATABASES
|
| 868 |
+
|
| 869 |
+
def score_path(p: str) -> int:
|
| 870 |
+
score = 0
|
| 871 |
+
bname = os.path.basename(p)
|
| 872 |
+
parent = os.path.basename(os.path.dirname(p))
|
| 873 |
+
lower_p = p.lower()
|
| 874 |
+
for bad in ("linkz", "www.", "t.me", "telegram", "sample", "trailer"):
|
| 875 |
+
if bad in lower_p:
|
| 876 |
+
score -= 50
|
| 877 |
+
if re.search(r"\(\d{4}\)", parent) or re.search(r"\(\d{4}\)", bname):
|
| 878 |
+
score += 20
|
| 879 |
+
if "-" in parent:
|
| 880 |
+
score += 10
|
| 881 |
+
if "." in parent or "," in parent:
|
| 882 |
+
score += 5
|
| 883 |
+
if re.search(r"\bIT\b", p):
|
| 884 |
+
score += 15
|
| 885 |
+
score += min(len(parent), 30)
|
| 886 |
+
return score
|
| 887 |
+
|
| 888 |
+
all_deleted_paths = []
|
| 889 |
+
all_deleted_dirs = []
|
| 890 |
+
|
| 891 |
+
for root_dir in media_roots:
|
| 892 |
+
if not os.path.isdir(root_dir):
|
| 893 |
+
continue
|
| 894 |
+
url_map = {}
|
| 895 |
+
for dirpath, _, filenames in os.walk(root_dir):
|
| 896 |
+
for fn in filenames:
|
| 897 |
+
if fn.lower().endswith(".strm"):
|
| 898 |
+
fp = os.path.join(dirpath, fn)
|
| 899 |
+
try:
|
| 900 |
+
with open(fp, "r", encoding="utf-8", errors="ignore") as f:
|
| 901 |
+
url = f.read().strip()
|
| 902 |
+
if url:
|
| 903 |
+
url_map.setdefault(url, []).append(fp)
|
| 904 |
+
except Exception:
|
| 905 |
+
pass
|
| 906 |
+
|
| 907 |
+
for url, filepaths in url_map.items():
|
| 908 |
+
if len(filepaths) <= 1:
|
| 909 |
+
continue
|
| 910 |
+
filepaths.sort(key=score_path, reverse=True)
|
| 911 |
+
canonical = filepaths[0]
|
| 912 |
+
duplicates = filepaths[1:]
|
| 913 |
+
|
| 914 |
+
for dup in duplicates:
|
| 915 |
+
dup_dir = os.path.dirname(dup)
|
| 916 |
+
canon_dir = os.path.dirname(canonical)
|
| 917 |
+
if dup_dir != canon_dir and dup_dir != root_dir:
|
| 918 |
+
print(f"[DEDUP] Removing duplicate media folder: '{dup_dir}' (retaining '{canon_dir}')")
|
| 919 |
+
try:
|
| 920 |
+
shutil.rmtree(dup_dir, ignore_errors=True)
|
| 921 |
+
all_deleted_dirs.append(dup_dir)
|
| 922 |
+
except Exception as e:
|
| 923 |
+
print(f"[DEDUP] Error removing folder {dup_dir}: {e}")
|
| 924 |
+
else:
|
| 925 |
+
print(f"[DEDUP] Removing duplicate media file: '{dup}' (retaining '{canonical}')")
|
| 926 |
+
try:
|
| 927 |
+
if os.path.exists(dup):
|
| 928 |
+
os.remove(dup)
|
| 929 |
+
except Exception as e:
|
| 930 |
+
print(f"[DEDUP] Error removing file {dup}: {e}")
|
| 931 |
+
all_deleted_paths.append(dup)
|
| 932 |
+
|
| 933 |
+
for db_p in db_list:
|
| 934 |
+
if not os.path.exists(db_p):
|
| 935 |
+
continue
|
| 936 |
+
try:
|
| 937 |
+
conn = sqlite3.connect(db_p, timeout=10)
|
| 938 |
+
cur = conn.cursor()
|
| 939 |
+
del_count = 0
|
| 940 |
+
for d in all_deleted_dirs:
|
| 941 |
+
cur.execute("DELETE FROM BaseItems WHERE Path = ? OR Path LIKE ?", (d, f"{d}/%"))
|
| 942 |
+
del_count += cur.rowcount
|
| 943 |
+
for p in all_deleted_paths:
|
| 944 |
+
cur.execute("DELETE FROM BaseItems WHERE Path = ?", (p,))
|
| 945 |
+
del_count += cur.rowcount
|
| 946 |
+
cur.execute("""
|
| 947 |
+
DELETE FROM BaseItems
|
| 948 |
+
WHERE Id NOT IN (
|
| 949 |
+
SELECT MIN(Id)
|
| 950 |
+
FROM BaseItems
|
| 951 |
+
GROUP BY Path
|
| 952 |
+
)
|
| 953 |
+
AND Path IS NOT NULL AND Path != ''
|
| 954 |
+
""")
|
| 955 |
+
del_count += cur.rowcount
|
| 956 |
+
conn.commit()
|
| 957 |
+
conn.close()
|
| 958 |
+
if del_count > 0:
|
| 959 |
+
print(f"[DEDUP] Cleaned {del_count} redundant BaseItems database row(s) in {db_p}")
|
| 960 |
+
except Exception as e:
|
| 961 |
+
print(f"[DEDUP] Note cleaning database {db_p}: {e}")
|
| 962 |
+
|
| 963 |
def main():
|
| 964 |
env_force = os.environ.get("RESET_JELLYFIN_PASSWORD", "").strip().lower() in ("1", "true", "yes")
|
| 965 |
manual_trigger = any(os.path.exists(p) for p in MANUAL_RESET_TRIGGERS)
|
|
|
|
| 990 |
heal_mismatched_media()
|
| 991 |
heal_channel_and_db_names()
|
| 992 |
heal_missing_db_strm_paths()
|
| 993 |
+
deduplicate_media_strms()
|
| 994 |
repair_user_subtitle_preferences()
|
| 995 |
repair_user_policy_preferences()
|
| 996 |
repair_system_xml()
|
apex/tests/test_repair_jellyfin_libraries.py
CHANGED
|
@@ -5,6 +5,7 @@ import tempfile
|
|
| 5 |
import pytest
|
| 6 |
|
| 7 |
from repair_jellyfin_libraries import (
|
|
|
|
| 8 |
repair_database,
|
| 9 |
cleanup_empty_media_folders,
|
| 10 |
heal_subtitle_files,
|
|
@@ -702,3 +703,56 @@ def test_heal_mismatched_media_merges_into_existing_destination(tmp_path):
|
|
| 702 |
|
| 703 |
|
| 704 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import pytest
|
| 6 |
|
| 7 |
from repair_jellyfin_libraries import (
|
| 8 |
+
deduplicate_media_strms,
|
| 9 |
repair_database,
|
| 10 |
cleanup_empty_media_folders,
|
| 11 |
heal_subtitle_files,
|
|
|
|
| 703 |
|
| 704 |
|
| 705 |
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
def test_deduplicate_media_strms_removes_duplicate_folders_and_db_rows(tmp_path):
|
| 709 |
+
movies_root = tmp_path / "Movies"
|
| 710 |
+
movies_root.mkdir()
|
| 711 |
+
|
| 712 |
+
# Canonical movie
|
| 713 |
+
canon_dir = movies_root / "Spider-Man Brand New Day (2026)"
|
| 714 |
+
canon_dir.mkdir()
|
| 715 |
+
canon_strm = canon_dir / "Spider-Man Brand New Day (2026).strm"
|
| 716 |
+
canon_strm.write_text("http://stream/119/video.mp4")
|
| 717 |
+
|
| 718 |
+
# Duplicate movie with noisy prefix
|
| 719 |
+
dup_dir = movies_root / "Man Brand New Day (2026)"
|
| 720 |
+
dup_dir.mkdir()
|
| 721 |
+
dup_strm = dup_dir / "Man Brand New Day (2026).strm"
|
| 722 |
+
dup_strm.write_text("http://stream/119/video.mp4")
|
| 723 |
+
|
| 724 |
+
# Setup test DB
|
| 725 |
+
db_file = tmp_path / "jellyfin.db"
|
| 726 |
+
conn = sqlite3.connect(str(db_file))
|
| 727 |
+
cur = conn.cursor()
|
| 728 |
+
cur.execute("""
|
| 729 |
+
CREATE TABLE BaseItems (
|
| 730 |
+
Id TEXT PRIMARY KEY,
|
| 731 |
+
Path TEXT,
|
| 732 |
+
Name TEXT,
|
| 733 |
+
type TEXT
|
| 734 |
+
)
|
| 735 |
+
""")
|
| 736 |
+
cur.execute("INSERT INTO BaseItems VALUES ('id1', ?, 'Spider-Man', 'Movie')", (str(canon_strm),))
|
| 737 |
+
cur.execute("INSERT INTO BaseItems VALUES ('id2', ?, 'Spider-Man', 'Movie')", (str(canon_dir),))
|
| 738 |
+
cur.execute("INSERT INTO BaseItems VALUES ('id3', ?, 'Man', 'Movie')", (str(dup_strm),))
|
| 739 |
+
cur.execute("INSERT INTO BaseItems VALUES ('id4', ?, 'Man', 'Movie')", (str(dup_dir),))
|
| 740 |
+
conn.commit()
|
| 741 |
+
conn.close()
|
| 742 |
+
|
| 743 |
+
deduplicate_media_strms(roots=[str(movies_root)], databases=[str(db_file)])
|
| 744 |
+
|
| 745 |
+
assert canon_dir.exists()
|
| 746 |
+
assert canon_strm.exists()
|
| 747 |
+
assert not dup_dir.exists()
|
| 748 |
+
assert not dup_strm.exists()
|
| 749 |
+
|
| 750 |
+
conn = sqlite3.connect(str(db_file))
|
| 751 |
+
cur = conn.cursor()
|
| 752 |
+
cur.execute("SELECT Path FROM BaseItems")
|
| 753 |
+
paths = [r[0] for r in cur.fetchall()]
|
| 754 |
+
conn.close()
|
| 755 |
+
|
| 756 |
+
assert str(canon_strm) in paths
|
| 757 |
+
assert str(dup_strm) not in paths
|
| 758 |
+
assert str(dup_dir) not in paths
|
apex/ui/abyss.css
CHANGED
|
@@ -238,17 +238,22 @@ input[type="radio"]:checked {
|
|
| 238 |
accent-color: rgb(var(--abyss-accent));
|
| 239 |
}
|
| 240 |
|
| 241 |
-
/* 4. TYPOGRAPHY */
|
| 242 |
|
| 243 |
-
.headerTabs
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
|
| 254 |
.pageTitle {
|
|
@@ -2559,7 +2564,7 @@ body:has(#videoOsdPage) .skinHeader .MuiTypography-root.MuiTypography-body1 {
|
|
| 2559 |
}
|
| 2560 |
|
| 2561 |
.headerTabs.sectionTabs {
|
| 2562 |
-
|
| 2563 |
}
|
| 2564 |
}
|
| 2565 |
|
|
@@ -2653,7 +2658,7 @@ body:has(#videoOsdPage) .skinHeader .MuiTypography-root.MuiTypography-body1 {
|
|
| 2653 |
}
|
| 2654 |
|
| 2655 |
body:has(#indexPage.homePage:not(.hide)) .headerTabs.sectionTabs {
|
| 2656 |
-
|
| 2657 |
}
|
| 2658 |
}
|
| 2659 |
|
|
@@ -2699,41 +2704,43 @@ body:has(#videoOsdPage) .skinHeader .MuiTypography-root.MuiTypography-body1 {
|
|
| 2699 |
|
| 2700 |
@media (min-aspect-ratio: 1.98/1) {
|
| 2701 |
.detailImageContainer .card {
|
| 2702 |
-
width:
|
|
|
|
| 2703 |
}
|
| 2704 |
|
| 2705 |
.detailRibbon {
|
| 2706 |
-
padding-left:
|
| 2707 |
}
|
| 2708 |
|
| 2709 |
.detailPagePrimaryContent,
|
| 2710 |
.layout-tv .detailPageSecondaryContainer.padded-left,
|
| 2711 |
.layout-desktop .detailPageSecondaryContainer.padded-left {
|
| 2712 |
-
padding-left:
|
| 2713 |
}
|
| 2714 |
|
| 2715 |
.detailPageContent {
|
| 2716 |
-
padding-left:
|
| 2717 |
}
|
| 2718 |
}
|
| 2719 |
|
| 2720 |
@media (min-aspect-ratio: 2.7/1) {
|
| 2721 |
.detailImageContainer .card {
|
| 2722 |
-
width:
|
|
|
|
| 2723 |
}
|
| 2724 |
|
| 2725 |
.detailRibbon {
|
| 2726 |
-
padding-left:
|
| 2727 |
}
|
| 2728 |
|
| 2729 |
.detailPagePrimaryContent,
|
| 2730 |
.layout-tv .detailPageSecondaryContainer.padded-left,
|
| 2731 |
.layout-desktop .detailPageSecondaryContainer.padded-left {
|
| 2732 |
-
padding-left:
|
| 2733 |
}
|
| 2734 |
|
| 2735 |
.detailPageContent {
|
| 2736 |
-
padding-left:
|
| 2737 |
}
|
| 2738 |
}
|
| 2739 |
|
|
@@ -4124,35 +4131,48 @@ button[data-action="search"] {
|
|
| 4124 |
margin-right: 14px !important;
|
| 4125 |
}
|
| 4126 |
|
| 4127 |
-
/* 1. Continue Watching & Next Up
|
| 4128 |
.resumeSection .card,
|
| 4129 |
.nextUpSection .card,
|
| 4130 |
div[data-category="resume"] .card,
|
| 4131 |
div[data-category="nextup"] .card {
|
| 4132 |
-
width:
|
| 4133 |
-
max-width:
|
| 4134 |
-
flex: 0 0
|
| 4135 |
}
|
| 4136 |
|
| 4137 |
.resumeSection .cardScalable,
|
| 4138 |
.nextUpSection .cardScalable,
|
| 4139 |
div[data-category="resume"] .cardScalable,
|
| 4140 |
div[data-category="nextup"] .cardScalable,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4141 |
.thumbCard .cardScalable,
|
| 4142 |
.backdropCard .cardScalable {
|
| 4143 |
-
aspect-ratio:
|
| 4144 |
border-radius: 12px !important;
|
| 4145 |
overflow: hidden !important;
|
| 4146 |
}
|
| 4147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4148 |
@media (max-width: 600px) {
|
| 4149 |
.resumeSection .card,
|
| 4150 |
.nextUpSection .card,
|
| 4151 |
div[data-category="resume"] .card,
|
| 4152 |
div[data-category="nextup"] .card {
|
| 4153 |
-
width:
|
| 4154 |
-
max-width:
|
| 4155 |
-
flex: 0 0
|
| 4156 |
}
|
| 4157 |
}
|
| 4158 |
|
|
@@ -4537,6 +4557,7 @@ header .pageTitle,
|
|
| 4537 |
/* ========================================================
|
| 4538 |
DETAIL PAGE: POSTER THUMBNAIL & METADATA SPACING FIX
|
| 4539 |
Prevents poster from overlapping title/metadata text
|
|
|
|
| 4540 |
======================================================== */
|
| 4541 |
#itemDetailPage .detailRibbon,
|
| 4542 |
.layout-mobile #itemDetailPage .detailRibbon,
|
|
@@ -4546,19 +4567,23 @@ header .pageTitle,
|
|
| 4546 |
display: flex !important;
|
| 4547 |
flex-direction: row !important;
|
| 4548 |
align-items: flex-start !important;
|
| 4549 |
-
gap:
|
| 4550 |
-
padding-left:
|
| 4551 |
-
padding-right:
|
|
|
|
| 4552 |
margin-top: 0 !important;
|
| 4553 |
box-sizing: border-box !important;
|
|
|
|
|
|
|
| 4554 |
}
|
| 4555 |
|
| 4556 |
#itemDetailPage .detailImageContainer,
|
| 4557 |
.detailImageContainer {
|
| 4558 |
position: relative !important;
|
| 4559 |
-
flex
|
| 4560 |
-
width:
|
| 4561 |
-
max-width:
|
|
|
|
| 4562 |
margin: 0 !important;
|
| 4563 |
z-index: 2 !important;
|
| 4564 |
}
|
|
@@ -4572,8 +4597,8 @@ header .pageTitle,
|
|
| 4572 |
top: auto !important;
|
| 4573 |
left: auto !important;
|
| 4574 |
margin: 0 !important;
|
| 4575 |
-
width:
|
| 4576 |
-
max-width:
|
| 4577 |
border-radius: 12px !important;
|
| 4578 |
}
|
| 4579 |
|
|
@@ -4581,17 +4606,27 @@ header .pageTitle,
|
|
| 4581 |
.layout-mobile #itemDetailPage .detailPagePrimaryContent,
|
| 4582 |
.layout-desktop #itemDetailPage .detailPagePrimaryContent,
|
| 4583 |
.layout-tv #itemDetailPage .detailPagePrimaryContent,
|
| 4584 |
-
.detailPagePrimaryContent
|
|
|
|
| 4585 |
flex: 1 1 auto !important;
|
| 4586 |
min-width: 0 !important;
|
|
|
|
|
|
|
| 4587 |
padding-left: 0 !important;
|
| 4588 |
margin-left: 0 !important;
|
| 4589 |
}
|
| 4590 |
|
|
|
|
| 4591 |
#itemDetailPage .itemName,
|
|
|
|
|
|
|
| 4592 |
.itemName {
|
| 4593 |
-
|
|
|
|
| 4594 |
overflow-wrap: break-word !important;
|
|
|
|
|
|
|
|
|
|
| 4595 |
}
|
| 4596 |
|
| 4597 |
/* Universal Abyss Spotlight Hero Banner Iframe */
|
|
|
|
| 238 |
accent-color: rgb(var(--abyss-accent));
|
| 239 |
}
|
| 240 |
|
| 241 |
+
/* 4. TYPOGRAPHY - Native Header Tabs Fully Suppressed in Favor of Apex TV Topbar */
|
| 242 |
|
| 243 |
+
.headerTabs,
|
| 244 |
+
.headerTabs.sectionTabs,
|
| 245 |
+
.emby-tabs,
|
| 246 |
+
.emby-tabs-slider,
|
| 247 |
+
.sectionTabs {
|
| 248 |
+
display: none !important;
|
| 249 |
+
visibility: hidden !important;
|
| 250 |
+
height: 0 !important;
|
| 251 |
+
width: 0 !important;
|
| 252 |
+
opacity: 0 !important;
|
| 253 |
+
pointer-events: none !important;
|
| 254 |
+
overflow: hidden !important;
|
| 255 |
+
margin: 0 !important;
|
| 256 |
+
padding: 0 !important;
|
| 257 |
}
|
| 258 |
|
| 259 |
.pageTitle {
|
|
|
|
| 2564 |
}
|
| 2565 |
|
| 2566 |
.headerTabs.sectionTabs {
|
| 2567 |
+
display: none !important;
|
| 2568 |
}
|
| 2569 |
}
|
| 2570 |
|
|
|
|
| 2658 |
}
|
| 2659 |
|
| 2660 |
body:has(#indexPage.homePage:not(.hide)) .headerTabs.sectionTabs {
|
| 2661 |
+
display: none !important;
|
| 2662 |
}
|
| 2663 |
}
|
| 2664 |
|
|
|
|
| 2704 |
|
| 2705 |
@media (min-aspect-ratio: 1.98/1) {
|
| 2706 |
.detailImageContainer .card {
|
| 2707 |
+
width: 150px !important;
|
| 2708 |
+
max-width: 150px !important;
|
| 2709 |
}
|
| 2710 |
|
| 2711 |
.detailRibbon {
|
| 2712 |
+
padding-left: 20px !important;
|
| 2713 |
}
|
| 2714 |
|
| 2715 |
.detailPagePrimaryContent,
|
| 2716 |
.layout-tv .detailPageSecondaryContainer.padded-left,
|
| 2717 |
.layout-desktop .detailPageSecondaryContainer.padded-left {
|
| 2718 |
+
padding-left: 0 !important;
|
| 2719 |
}
|
| 2720 |
|
| 2721 |
.detailPageContent {
|
| 2722 |
+
padding-left: 0 !important;
|
| 2723 |
}
|
| 2724 |
}
|
| 2725 |
|
| 2726 |
@media (min-aspect-ratio: 2.7/1) {
|
| 2727 |
.detailImageContainer .card {
|
| 2728 |
+
width: 150px !important;
|
| 2729 |
+
max-width: 150px !important;
|
| 2730 |
}
|
| 2731 |
|
| 2732 |
.detailRibbon {
|
| 2733 |
+
padding-left: 20px !important;
|
| 2734 |
}
|
| 2735 |
|
| 2736 |
.detailPagePrimaryContent,
|
| 2737 |
.layout-tv .detailPageSecondaryContainer.padded-left,
|
| 2738 |
.layout-desktop .detailPageSecondaryContainer.padded-left {
|
| 2739 |
+
padding-left: 0 !important;
|
| 2740 |
}
|
| 2741 |
|
| 2742 |
.detailPageContent {
|
| 2743 |
+
padding-left: 0 !important;
|
| 2744 |
}
|
| 2745 |
}
|
| 2746 |
|
|
|
|
| 4131 |
margin-right: 14px !important;
|
| 4132 |
}
|
| 4133 |
|
| 4134 |
+
/* 1. Continue Watching & Next Up: Uniform 2:3 Vertical Rectangular Posters */
|
| 4135 |
.resumeSection .card,
|
| 4136 |
.nextUpSection .card,
|
| 4137 |
div[data-category="resume"] .card,
|
| 4138 |
div[data-category="nextup"] .card {
|
| 4139 |
+
width: 150px !important;
|
| 4140 |
+
max-width: 150px !important;
|
| 4141 |
+
flex: 0 0 150px !important;
|
| 4142 |
}
|
| 4143 |
|
| 4144 |
.resumeSection .cardScalable,
|
| 4145 |
.nextUpSection .cardScalable,
|
| 4146 |
div[data-category="resume"] .cardScalable,
|
| 4147 |
div[data-category="nextup"] .cardScalable,
|
| 4148 |
+
.resumeSection .card[data-type="Episode"] .cardScalable,
|
| 4149 |
+
.nextUpSection .card[data-type="Episode"] .cardScalable,
|
| 4150 |
+
div[data-category="resume"] .card[data-type="Episode"] .cardScalable,
|
| 4151 |
+
div[data-category="nextup"] .card[data-type="Episode"] .cardScalable,
|
| 4152 |
.thumbCard .cardScalable,
|
| 4153 |
.backdropCard .cardScalable {
|
| 4154 |
+
aspect-ratio: 2 / 3 !important;
|
| 4155 |
border-radius: 12px !important;
|
| 4156 |
overflow: hidden !important;
|
| 4157 |
}
|
| 4158 |
|
| 4159 |
+
.resumeSection .cardImageContainer img,
|
| 4160 |
+
.nextUpSection .cardImageContainer img,
|
| 4161 |
+
div[data-category="resume"] .cardImageContainer img,
|
| 4162 |
+
div[data-category="nextup"] .cardImageContainer img {
|
| 4163 |
+
object-fit: cover !important;
|
| 4164 |
+
width: 100% !important;
|
| 4165 |
+
height: 100% !important;
|
| 4166 |
+
}
|
| 4167 |
+
|
| 4168 |
@media (max-width: 600px) {
|
| 4169 |
.resumeSection .card,
|
| 4170 |
.nextUpSection .card,
|
| 4171 |
div[data-category="resume"] .card,
|
| 4172 |
div[data-category="nextup"] .card {
|
| 4173 |
+
width: 125px !important;
|
| 4174 |
+
max-width: 125px !important;
|
| 4175 |
+
flex: 0 0 125px !important;
|
| 4176 |
}
|
| 4177 |
}
|
| 4178 |
|
|
|
|
| 4557 |
/* ========================================================
|
| 4558 |
DETAIL PAGE: POSTER THUMBNAIL & METADATA SPACING FIX
|
| 4559 |
Prevents poster from overlapping title/metadata text
|
| 4560 |
+
Ensures title never breaks letter-by-letter in landscape or portrait
|
| 4561 |
======================================================== */
|
| 4562 |
#itemDetailPage .detailRibbon,
|
| 4563 |
.layout-mobile #itemDetailPage .detailRibbon,
|
|
|
|
| 4567 |
display: flex !important;
|
| 4568 |
flex-direction: row !important;
|
| 4569 |
align-items: flex-start !important;
|
| 4570 |
+
gap: 20px !important;
|
| 4571 |
+
padding-left: 20px !important;
|
| 4572 |
+
padding-right: 20px !important;
|
| 4573 |
+
padding-top: 16px !important;
|
| 4574 |
margin-top: 0 !important;
|
| 4575 |
box-sizing: border-box !important;
|
| 4576 |
+
width: 100% !important;
|
| 4577 |
+
max-width: 100% !important;
|
| 4578 |
}
|
| 4579 |
|
| 4580 |
#itemDetailPage .detailImageContainer,
|
| 4581 |
.detailImageContainer {
|
| 4582 |
position: relative !important;
|
| 4583 |
+
flex: 0 0 150px !important;
|
| 4584 |
+
width: 150px !important;
|
| 4585 |
+
max-width: 150px !important;
|
| 4586 |
+
min-width: 130px !important;
|
| 4587 |
margin: 0 !important;
|
| 4588 |
z-index: 2 !important;
|
| 4589 |
}
|
|
|
|
| 4597 |
top: auto !important;
|
| 4598 |
left: auto !important;
|
| 4599 |
margin: 0 !important;
|
| 4600 |
+
width: 100% !important;
|
| 4601 |
+
max-width: 100% !important;
|
| 4602 |
border-radius: 12px !important;
|
| 4603 |
}
|
| 4604 |
|
|
|
|
| 4606 |
.layout-mobile #itemDetailPage .detailPagePrimaryContent,
|
| 4607 |
.layout-desktop #itemDetailPage .detailPagePrimaryContent,
|
| 4608 |
.layout-tv #itemDetailPage .detailPagePrimaryContent,
|
| 4609 |
+
.detailPagePrimaryContent,
|
| 4610 |
+
#itemDetailPage .detailPageContent {
|
| 4611 |
flex: 1 1 auto !important;
|
| 4612 |
min-width: 0 !important;
|
| 4613 |
+
width: auto !important;
|
| 4614 |
+
max-width: 100% !important;
|
| 4615 |
padding-left: 0 !important;
|
| 4616 |
margin-left: 0 !important;
|
| 4617 |
}
|
| 4618 |
|
| 4619 |
+
#itemDetailPage .nameContainer,
|
| 4620 |
#itemDetailPage .itemName,
|
| 4621 |
+
#itemDetailPage .parentName,
|
| 4622 |
+
#itemDetailPage h1.itemName,
|
| 4623 |
.itemName {
|
| 4624 |
+
white-space: normal !important;
|
| 4625 |
+
word-break: normal !important;
|
| 4626 |
overflow-wrap: break-word !important;
|
| 4627 |
+
display: block !important;
|
| 4628 |
+
width: auto !important;
|
| 4629 |
+
max-width: 100% !important;
|
| 4630 |
}
|
| 4631 |
|
| 4632 |
/* Universal Abyss Spotlight Hero Banner Iframe */
|
apex/ui/spotlight-loader.js
CHANGED
|
@@ -794,7 +794,7 @@ function findHomeTab() {
|
|
| 794 |
'.headerCastButton, .btnCast, button[data-action="cast"], button[data-action="syncplay"], ' +
|
| 795 |
'button[title*="SyncPlay" i], button[aria-label*="SyncPlay" i], button[title*="Cast" i], button[aria-label*="Cast" i], ' +
|
| 796 |
'button:has(svg[data-testid="CastIcon"]), button:has(svg[data-testid="GroupIcon"]), button:has(svg[data-testid="PeopleIcon"]), ' +
|
| 797 |
-
'.headerTop .pageTitle, .skinHeader .pageTitle, .headerLogoText, .pageTitleWithDefaultLogo, .headerTabs.sectionTabs'
|
| 798 |
);
|
| 799 |
for (var u = 0; u < unwanted.length; u++) {
|
| 800 |
unwanted[u].style.display = "none";
|
|
|
|
| 794 |
'.headerCastButton, .btnCast, button[data-action="cast"], button[data-action="syncplay"], ' +
|
| 795 |
'button[title*="SyncPlay" i], button[aria-label*="SyncPlay" i], button[title*="Cast" i], button[aria-label*="Cast" i], ' +
|
| 796 |
'button:has(svg[data-testid="CastIcon"]), button:has(svg[data-testid="GroupIcon"]), button:has(svg[data-testid="PeopleIcon"]), ' +
|
| 797 |
+
'.headerTop .pageTitle, .skinHeader .pageTitle, .headerLogoText, .pageTitleWithDefaultLogo, .headerTabs, .sectionTabs, .headerTabs.sectionTabs, .emby-tabs, .emby-tabs-slider'
|
| 798 |
);
|
| 799 |
for (var u = 0; u < unwanted.length; u++) {
|
| 800 |
unwanted[u].style.display = "none";
|