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 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.sectionTabs {
244
- text-size-adjust: 110%;
245
- font-size: 110%;
246
- padding-top: 8px;
247
- margin-top: -2em;
248
- width: fit-content;
249
- max-width: 95vw;
250
- align-self: center !important;
251
- border-radius: 50px;
 
 
 
 
 
252
  }
253
 
254
  .pageTitle {
@@ -2559,7 +2564,7 @@ body:has(#videoOsdPage) .skinHeader .MuiTypography-root.MuiTypography-body1 {
2559
  }
2560
 
2561
  .headerTabs.sectionTabs {
2562
- margin-top: -4.9em !important;
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
- margin-top: -4.9em !important;
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: 21vw !important;
 
2703
  }
2704
 
2705
  .detailRibbon {
2706
- padding-left: 28.45vw !important;
2707
  }
2708
 
2709
  .detailPagePrimaryContent,
2710
  .layout-tv .detailPageSecondaryContainer.padded-left,
2711
  .layout-desktop .detailPageSecondaryContainer.padded-left {
2712
- padding-left: 28.45vw !important;
2713
  }
2714
 
2715
  .detailPageContent {
2716
- padding-left: 28.45vw !important;
2717
  }
2718
  }
2719
 
2720
  @media (min-aspect-ratio: 2.7/1) {
2721
  .detailImageContainer .card {
2722
- width: 16vw !important;
 
2723
  }
2724
 
2725
  .detailRibbon {
2726
- padding-left: 23.45vw !important;
2727
  }
2728
 
2729
  .detailPagePrimaryContent,
2730
  .layout-tv .detailPageSecondaryContainer.padded-left,
2731
  .layout-desktop .detailPageSecondaryContainer.padded-left {
2732
- padding-left: 23.45vw !important;
2733
  }
2734
 
2735
  .detailPageContent {
2736
- padding-left: 23.45vw !important;
2737
  }
2738
  }
2739
 
@@ -4124,35 +4131,48 @@ button[data-action="search"] {
4124
  margin-right: 14px !important;
4125
  }
4126
 
4127
- /* 1. Continue Watching & Next Up (16:9 Landscape) */
4128
  .resumeSection .card,
4129
  .nextUpSection .card,
4130
  div[data-category="resume"] .card,
4131
  div[data-category="nextup"] .card {
4132
- width: 280px !important;
4133
- max-width: 280px !important;
4134
- flex: 0 0 280px !important;
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: 16 / 9 !important;
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: 220px !important;
4154
- max-width: 220px !important;
4155
- flex: 0 0 220px !important;
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: 18px !important;
4550
- padding-left: 16px !important;
4551
- padding-right: 16px !important;
 
4552
  margin-top: 0 !important;
4553
  box-sizing: border-box !important;
 
 
4554
  }
4555
 
4556
  #itemDetailPage .detailImageContainer,
4557
  .detailImageContainer {
4558
  position: relative !important;
4559
- flex-shrink: 0 !important;
4560
- width: 140px !important;
4561
- max-width: 140px !important;
 
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: 140px !important;
4576
- max-width: 140px !important;
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
- word-break: break-word !important;
 
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";