Hermes Agent commited on
Commit
aae3fe7
·
1 Parent(s): 13c408a

fix(all): restore spotlight hero banner, fix detail poster overlap, enable reliable Home/Movies/TV navigation, and prevent STRM pruning playback errors

Browse files
apex/repair_jellyfin_libraries.py CHANGED
@@ -531,9 +531,15 @@ def heal_mismatched_media(roots=None, databases=None):
531
  conn.close()
532
  except Exception:
533
  pass
534
- elif f.endswith(".strm") or f.lower() in ("movie.nfo", "tvshow.nfo"):
535
  try:
536
- os.remove(src_f)
 
 
 
 
 
 
537
  except Exception:
538
  pass
539
  try:
@@ -558,8 +564,11 @@ def heal_mismatched_media(roots=None, databases=None):
558
  new_strm = os.path.join(dirpath, f"{clean_strm_base}{ext}")
559
  if not os.path.exists(new_strm):
560
  try:
561
- os.rename(old_strm, new_strm)
562
- print(f"[REPAIR] Cleaned strm file: '{fn}' -> '{clean_strm_base}{ext}'")
 
 
 
563
  for db_p in db_list:
564
  if os.path.exists(db_p):
565
  try:
@@ -570,7 +579,7 @@ def heal_mismatched_media(roots=None, databases=None):
570
  except Exception:
571
  pass
572
  except Exception as strm_err:
573
- print(f"[REPAIR] Note renaming strm {old_strm}: {strm_err}")
574
  elif fn.lower() in ("movie.nfo", "tvshow.nfo"):
575
  _heal_nfo_file(os.path.join(dirpath, fn))
576
 
@@ -784,6 +793,63 @@ def repair_system_xml(configs=None):
784
  print(f"[REPAIR] Note repairing {cfg}: {e}")
785
 
786
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
787
  def main():
788
  env_force = os.environ.get("RESET_JELLYFIN_PASSWORD", "").strip().lower() in ("1", "true", "yes")
789
  manual_trigger = any(os.path.exists(p) for p in MANUAL_RESET_TRIGGERS)
@@ -813,6 +879,7 @@ def main():
813
  heal_subtitle_files()
814
  heal_mismatched_media()
815
  heal_channel_and_db_names()
 
816
  repair_user_subtitle_preferences()
817
  repair_user_policy_preferences()
818
  repair_system_xml()
 
531
  conn.close()
532
  except Exception:
533
  pass
534
+ elif f.endswith(".strm"):
535
  try:
536
+ if not os.path.exists(dst_f):
537
+ shutil.copy2(src_f, dst_f)
538
+ elif not os.path.exists(src_f):
539
+ try:
540
+ os.link(dst_f, src_f)
541
+ except OSError:
542
+ shutil.copy2(dst_f, src_f)
543
  except Exception:
544
  pass
545
  try:
 
564
  new_strm = os.path.join(dirpath, f"{clean_strm_base}{ext}")
565
  if not os.path.exists(new_strm):
566
  try:
567
+ try:
568
+ os.link(old_strm, new_strm)
569
+ except OSError:
570
+ shutil.copy2(old_strm, new_strm)
571
+ print(f"[REPAIR] Created clean alias strm: '{fn}' -> '{clean_strm_base}{ext}'")
572
  for db_p in db_list:
573
  if os.path.exists(db_p):
574
  try:
 
579
  except Exception:
580
  pass
581
  except Exception as strm_err:
582
+ print(f"[REPAIR] Note creating strm alias {old_strm}: {strm_err}")
583
  elif fn.lower() in ("movie.nfo", "tvshow.nfo"):
584
  _heal_nfo_file(os.path.join(dirpath, fn))
585
 
 
793
  print(f"[REPAIR] Note repairing {cfg}: {e}")
794
 
795
 
796
+
797
+ def heal_missing_db_strm_paths(databases=None):
798
+ """Ensures every single .strm path recorded in Jellyfin BaseItems actually exists on disk.
799
+ If a file is missing due to prior pruning or dot/dash naming differences, reconstructs
800
+ or hardlinks it immediately from matching media on disk."""
801
+ db_list = databases or TARGET_DATABASES
802
+ for db_p in db_list:
803
+ if not os.path.exists(db_p):
804
+ continue
805
+ try:
806
+ conn = sqlite3.connect(db_p, timeout=10)
807
+ cur = conn.cursor()
808
+ cur.execute("SELECT Id, Path, Name FROM BaseItems WHERE Path LIKE '%.strm'")
809
+ rows = cur.fetchall()
810
+ repaired = 0
811
+ for row_id, path, name in rows:
812
+ if not path or os.path.exists(path):
813
+ continue
814
+ parent_dir = os.path.dirname(path)
815
+ file_name = os.path.basename(path)
816
+ found_src = None
817
+
818
+ # 1. Search in the same parent directory for any .strm
819
+ if os.path.isdir(parent_dir):
820
+ candidates = [os.path.join(parent_dir, f) for f in os.listdir(parent_dir) if f.lower().endswith(".strm")]
821
+ if candidates:
822
+ found_src = candidates[0]
823
+
824
+ # 2. If parent_dir doesn't exist, search grandparent directory for a matching folder
825
+ if not found_src:
826
+ grandparent = os.path.dirname(parent_dir)
827
+ parent_name = os.path.basename(parent_dir)
828
+ if os.path.isdir(grandparent):
829
+ norm_parent = re.sub(r"[^a-zA-Z0-9]", "", parent_name).lower()
830
+ for sib in os.listdir(grandparent):
831
+ sib_path = os.path.join(grandparent, sib)
832
+ if os.path.isdir(sib_path):
833
+ if re.sub(r"[^a-zA-Z0-9]", "", sib).lower() == norm_parent:
834
+ cands = [os.path.join(sib_path, f) for f in os.listdir(sib_path) if f.lower().endswith(".strm")]
835
+ if cands:
836
+ found_src = cands[0]
837
+ break
838
+
839
+ if found_src and os.path.exists(found_src):
840
+ os.makedirs(parent_dir, exist_ok=True)
841
+ try:
842
+ os.link(found_src, path)
843
+ except OSError:
844
+ shutil.copy2(found_src, path)
845
+ repaired += 1
846
+ print(f"[REPAIR] Healed missing DB STRM: '{path}' <- '{found_src}'")
847
+ conn.close()
848
+ if repaired:
849
+ print(f"[REPAIR] Successfully healed {repaired} missing STRM file(s) for DB {db_p}")
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)
 
879
  heal_subtitle_files()
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()
apex/tg_streamer.py CHANGED
@@ -5373,53 +5373,10 @@ async def subtitles_sync(request):
5373
 
5374
 
5375
  async def _prune_orphan_strms(movies_dir, shows_dir, keep):
5376
- """Delete .strm files that point at our streamer but aren't in `keep`.
5377
-
5378
- During a folder-layout migration Jellyfin indexes both the stale flat .strm
5379
- and the new per-title folder .strm for the same Telegram file -> duplicates.
5380
- Only files whose contents reference our own streamer are touched, so
5381
- unrelated .strm files are never removed.
5382
- """
5383
- removed = 0
5384
- for root_dir in (movies_dir, shows_dir):
5385
- if not os.path.isdir(root_dir):
5386
- continue
5387
- for root, _, files in os.walk(root_dir):
5388
- for f in files:
5389
- if not f.lower().endswith(".strm"):
5390
- continue
5391
- path = os.path.normpath(os.path.join(root, f))
5392
- if path in keep:
5393
- continue
5394
- try:
5395
- with open(path, "r", encoding="utf-8", errors="ignore") as fh:
5396
- content = fh.read(256)
5397
- except OSError:
5398
- continue
5399
- stream_port = os.environ.get(
5400
- "TG_LISTEN_PORT", os.environ.get("PORT", "8080")
5401
- )
5402
- is_our_stream = (
5403
- "/stream/" in content
5404
- or f"127.0.0.1:{stream_port}" in content
5405
- or f":{stream_port}/" in content
5406
- )
5407
- if not is_our_stream:
5408
- continue
5409
- try:
5410
- os.remove(path)
5411
- removed += 1
5412
- logger.info(f"[RESTORE] Pruned orphaned .strm: {path}")
5413
- parent_dir = os.path.dirname(path)
5414
- if os.path.isdir(parent_dir) and not os.listdir(parent_dir):
5415
- try:
5416
- os.rmdir(parent_dir)
5417
- logger.info(f"[RESTORE] Cleaned empty parent folder: {parent_dir}")
5418
- except OSError:
5419
- pass
5420
- except OSError as e:
5421
- logger.warning(f"[RESTORE] Could not prune {path}: {e}")
5422
- return removed
5423
 
5424
 
5425
  async def restore_cached_strm_files():
 
5373
 
5374
 
5375
  async def _prune_orphan_strms(movies_dir, shows_dir, keep):
5376
+ """Safely preserve all .strm files. Deletion is permanently disabled to ensure
5377
+ seamless media playback across all library naming variants."""
5378
+ logger.info("[RESTORE] Automatic .strm pruning is safely disabled to safeguard media playback.")
5379
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5380
 
5381
 
5382
  async def restore_cached_strm_files():
apex/ui/abyss.css CHANGED
@@ -4306,8 +4306,8 @@ div[data-category="nextup"] .cardScalable,
4306
  align-items: center !important;
4307
  padding-left: 16px !important;
4308
  padding-right: 16px !important;
4309
- z-index: 10001 !important;
4310
- pointer-events: none !important;
4311
  }
4312
 
4313
  #apex-tv-topbar .apex-tv-tabs {
@@ -4387,12 +4387,9 @@ div[data-category="nextup"] .cardScalable,
4387
  }
4388
  }
4389
 
4390
- /* Hide topbar when video player or settings are active */
4391
- body:has(#videoOsdPage:not(.hide)) #apex-tv-topbar,
4392
- body:has(.videoOsd:not(.hide)) #apex-tv-topbar,
4393
- body:has(.nowPlayingPage:not(.hide)) #apex-tv-topbar,
4394
- body:has(#dashboardPage:not(.hide)) #apex-tv-topbar,
4395
- body.jellyfin-settings-page #apex-tv-topbar {
4396
  display: none !important;
4397
  opacity: 0 !important;
4398
  pointer-events: none !important;
@@ -4453,3 +4450,114 @@ body.jellyfin-settings-page #apex-tv-topbar {
4453
  box-sizing: border-box !important;
4454
  }
4455
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4306
  align-items: center !important;
4307
  padding-left: 16px !important;
4308
  padding-right: 16px !important;
4309
+ z-index: 99999 !important;
4310
+ pointer-events: auto !important;
4311
  }
4312
 
4313
  #apex-tv-topbar .apex-tv-tabs {
 
4387
  }
4388
  }
4389
 
4390
+ /* Hide topbar only during active fullscreen video playback or settings */
4391
+ body.videoPlayerContainerVisible #apex-tv-topbar,
4392
+ body.hide-apex-topbar #apex-tv-topbar {
 
 
 
4393
  display: none !important;
4394
  opacity: 0 !important;
4395
  pointer-events: none !important;
 
4450
  box-sizing: border-box !important;
4451
  }
4452
  }
4453
+
4454
+
4455
+ /* ========================================================
4456
+ DETAIL PAGE: POSTER THUMBNAIL & METADATA SPACING FIX
4457
+ Prevents poster from overlapping title/metadata text
4458
+ ======================================================== */
4459
+ #itemDetailPage .detailRibbon,
4460
+ .layout-mobile #itemDetailPage .detailRibbon,
4461
+ .layout-desktop #itemDetailPage .detailRibbon,
4462
+ .layout-tv #itemDetailPage .detailRibbon,
4463
+ .detailRibbon {
4464
+ display: flex !important;
4465
+ flex-direction: row !important;
4466
+ align-items: flex-start !important;
4467
+ gap: 18px !important;
4468
+ padding-left: 16px !important;
4469
+ padding-right: 16px !important;
4470
+ margin-top: 0 !important;
4471
+ box-sizing: border-box !important;
4472
+ }
4473
+
4474
+ #itemDetailPage .detailImageContainer,
4475
+ .detailImageContainer {
4476
+ position: relative !important;
4477
+ flex-shrink: 0 !important;
4478
+ width: 140px !important;
4479
+ max-width: 140px !important;
4480
+ margin: 0 !important;
4481
+ z-index: 2 !important;
4482
+ }
4483
+
4484
+ #itemDetailPage .detailImageContainer .card,
4485
+ .layout-mobile #itemDetailPage .detailImageContainer .card,
4486
+ .layout-desktop #itemDetailPage .detailImageContainer .card,
4487
+ .layout-tv #itemDetailPage .detailImageContainer .card,
4488
+ .detailImageContainer .card {
4489
+ position: relative !important;
4490
+ top: auto !important;
4491
+ left: auto !important;
4492
+ margin: 0 !important;
4493
+ width: 140px !important;
4494
+ max-width: 140px !important;
4495
+ border-radius: 12px !important;
4496
+ }
4497
+
4498
+ #itemDetailPage .detailPagePrimaryContent,
4499
+ .layout-mobile #itemDetailPage .detailPagePrimaryContent,
4500
+ .layout-desktop #itemDetailPage .detailPagePrimaryContent,
4501
+ .layout-tv #itemDetailPage .detailPagePrimaryContent,
4502
+ .detailPagePrimaryContent {
4503
+ flex: 1 1 auto !important;
4504
+ min-width: 0 !important;
4505
+ padding-left: 0 !important;
4506
+ margin-left: 0 !important;
4507
+ }
4508
+
4509
+ #itemDetailPage .itemName,
4510
+ .itemName {
4511
+ word-break: break-word !important;
4512
+ overflow-wrap: break-word !important;
4513
+ }
4514
+
4515
+ /* Universal Abyss Spotlight Hero Banner Iframe */
4516
+ iframe.featurediframe {
4517
+ width: 100% !important;
4518
+ display: block !important;
4519
+ border: none !important;
4520
+ margin: 0 !important;
4521
+ padding: 0 !important;
4522
+ min-height: 380px !important;
4523
+ height: 52vh !important;
4524
+ opacity: 1 !important;
4525
+ visibility: visible !important;
4526
+ }
4527
+
4528
+ @media (orientation: landscape) {
4529
+ iframe.featurediframe {
4530
+ height: 70vh !important;
4531
+ min-height: 340px !important;
4532
+ }
4533
+ }
4534
+
4535
+ /* Suppress Cast, SyncPlay, Menu/Hamburger buttons */
4536
+ button[aria-label="Menu"],
4537
+ button[aria-label="menu"],
4538
+ button:has(svg[data-testid="MenuIcon"]),
4539
+ .claude-stepped-menu,
4540
+ .mainDrawerButton,
4541
+ .headerButtonMenu,
4542
+ .headerSyncplayButton,
4543
+ .btnSyncPlay,
4544
+ .headerCastButton,
4545
+ .btnCast,
4546
+ button[data-action="cast"],
4547
+ button[data-action="syncplay"],
4548
+ button[title*="SyncPlay" i],
4549
+ button[aria-label*="SyncPlay" i],
4550
+ button[title*="Cast" i],
4551
+ button[aria-label*="Cast" i],
4552
+ button:has(svg[data-testid="CastIcon"]),
4553
+ button:has(svg[data-testid="GroupIcon"]),
4554
+ button:has(svg[data-testid="PeopleIcon"]),
4555
+ .headerLeft,
4556
+ .headerTabs.sectionTabs {
4557
+ display: none !important;
4558
+ visibility: hidden !important;
4559
+ pointer-events: none !important;
4560
+ width: 0 !important;
4561
+ height: 0 !important;
4562
+ opacity: 0 !important;
4563
+ }
apex/ui/spotlight-loader.js CHANGED
@@ -262,12 +262,16 @@
262
  });
263
 
264
  function isRouteVisible(indexPage, homeTab) {
265
- if (!indexPage || !indexPage.isConnected || !homeTab || !homeTab.isConnected) return false;
266
  if (document.hidden) return false;
267
- if (indexPage.classList.contains("hide") || indexPage.hidden) return false;
268
- var style = window.getComputedStyle(homeTab);
269
- if (style.display === "none") return false;
270
- return homeTab.offsetParent !== null || homeTab.getClientRects().length > 0 || !homeTab.classList.contains("hide");
 
 
 
 
271
  }
272
 
273
  function connectLifecycle(indexPage, homeTab, favoritesTab, iframe) {
@@ -328,60 +332,78 @@
328
 
329
  var currentSync = null;
330
 
331
- function findVisibleById(id) {
332
- var candidates = document.querySelectorAll('[id="' + id + '"]');
333
- for (var i = 0; i < candidates.length; i++) {
334
- var el = candidates[i];
335
- if (
336
- el.isConnected &&
337
- !el.classList.contains("hide") &&
338
- !el.hidden
339
- ) {
340
- return el;
341
- }
342
  }
343
- return candidates.length ? candidates[candidates.length - 1] : null;
 
 
 
 
344
  }
345
 
346
  function installSpotlight() {
347
  var installed = false;
348
  safe(function () {
349
- var indexPage = findVisibleById("indexPage");
350
- if (!indexPage) {
351
  clearSpotlightLifecycle();
352
  return;
353
  }
354
- var homeTab = indexPage.querySelector("#homeTab");
 
355
  if (!homeTab) {
356
- clearSpotlightLifecycle();
357
  return;
358
  }
359
- var favoritesTab = indexPage.querySelector("#favoritesTab");
 
 
360
 
361
  installFrameStyle();
362
  installBackdropOverlay();
363
 
364
  var iframe = homeTab.querySelector ? homeTab.querySelector("." + FRAME_CLASS) : null;
 
 
 
 
365
  if (!iframe) {
366
  iframe = document.createElement("iframe");
367
  iframe.className = FRAME_CLASS + " focusable";
368
  iframe.setAttribute("tabindex", "0");
369
  iframe.src = spotlightUrl;
370
  iframe.title = "Abyss Spotlight";
 
 
 
371
  iframe.addEventListener("blur", function () { spotlightFocused = false; });
372
- var sections = homeTab.querySelector ? homeTab.querySelector(".sections") : null;
 
 
 
373
  homeTab.insertBefore(iframe, sections || homeTab.firstChild);
374
  }
375
 
376
- safe(function () {
377
- if (indexPage.classList) indexPage.classList.add("abyss-spotlight-active");
378
- if (homeTab.classList) homeTab.classList.add("abyss-spotlight-active");
379
- });
 
 
 
380
 
381
  if (
382
  indexPage !== currentIndexPage ||
383
  homeTab !== currentHomeTab ||
384
- favoritesTab !== currentFavoritesTab ||
385
  iframe !== currentIframe
386
  ) {
387
  currentIndexPage = indexPage;
@@ -445,7 +467,7 @@
445
  var logoSvgUrl = (window.location.pathname.indexOf("/web") !== -1) ? "ui/apex-logo.svg" : "/jellyfin/web/ui/apex-logo.svg";
446
 
447
  // 1. Favicon sync
448
- var links = document.querySelectorAll("link[rel*='icon']");
449
  for (var i = 0; i < links.length; i++) {
450
  if (links[i].getAttribute("data-apex-logo") !== "true") {
451
  links[i].setAttribute("data-apex-logo", "true");
@@ -597,7 +619,7 @@
597
  }, 400);
598
  }
599
 
600
- var libIds = { movies: null, tvshows: null };
601
  var librariesDiscovered = false;
602
 
603
  function discoverLibraries() {
@@ -635,53 +657,27 @@
635
  return;
636
  }
637
 
638
- if (name === "Movies") {
639
- var cards = document.querySelectorAll('.card[data-type="CollectionFolder"], [data-category="views"] .card');
640
- for (var i = 0; i < cards.length; i++) {
641
- if ((cards[i].textContent || "").toLowerCase().indexOf("movie") !== -1) {
642
- cards[i].click();
643
- return;
644
- }
645
- }
646
- if (libIds.movies) {
647
- var url = "/details?id=" + libIds.movies;
648
- if (window.Emby && window.Emby.Page && typeof window.Emby.Page.show === "function") {
649
- window.Emby.Page.show(url);
650
- return;
651
- }
652
- window.location.hash = "#" + url;
653
  return;
654
  }
655
- window.location.hash = "#/movies";
656
- return;
657
- }
658
-
659
- if (name === "TV Shows") {
660
- var tvCards = document.querySelectorAll('.card[data-type="CollectionFolder"], [data-category="views"] .card');
661
- for (var t = 0; t < tvCards.length; t++) {
662
- var text = (tvCards[t].textContent || "").toLowerCase();
663
- if (text.indexOf("tv") !== -1 || text.indexOf("show") !== -1 || text.indexOf("series") !== -1) {
664
- tvCards[t].click();
665
- return;
666
- }
667
- }
668
- if (libIds.tvshows) {
669
- var url = "/details?id=" + libIds.tvshows;
670
- if (window.Emby && window.Emby.Page && typeof window.Emby.Page.show === "function") {
671
- window.Emby.Page.show(url);
672
- return;
673
- }
674
- window.location.hash = "#" + url;
675
  return;
676
  }
677
- window.location.hash = "#/shows";
678
  return;
679
  }
 
680
  }
681
 
682
  function updateActiveNavTab() {
683
  var hash = window.location.hash || "";
684
- var links = document.querySelectorAll(".apex-tv-tab, .apex-nav-item");
685
  for (var i = 0; i < links.length; i++) {
686
  var item = links[i];
687
  var navType = item.getAttribute("data-nav");
@@ -751,6 +747,17 @@
751
  document.body.appendChild(tvTopbar);
752
  }
753
 
 
 
 
 
 
 
 
 
 
 
 
754
  // 2. Hide Hamburger, Cast, SyncPlay, and stray drawer buttons completely
755
  var unwanted = document.querySelectorAll(
756
  'button[aria-label="Menu"], button[aria-label="menu"], ' +
 
262
  });
263
 
264
  function isRouteVisible(indexPage, homeTab) {
265
+ if (!homeTab || !homeTab.isConnected) return false;
266
  if (document.hidden) return false;
267
+ var hash = window.location.hash || "";
268
+ if (hash && hash !== "#/" && hash !== "#" && hash.indexOf("/home") === -1) {
269
+ if (hash.indexOf("details") !== -1 || hash.indexOf("search") !== -1 || hash.indexOf("settings") !== -1 || hash.indexOf("dashboard") !== -1 || hash.indexOf("video") !== -1) {
270
+ return false;
271
+ }
272
+ }
273
+ if (indexPage && (indexPage.classList.contains("hide") || indexPage.hidden)) return false;
274
+ return true;
275
  }
276
 
277
  function connectLifecycle(indexPage, homeTab, favoritesTab, iframe) {
 
332
 
333
  var currentSync = null;
334
 
335
+ function findHomeTab() {
336
+ var homeTab = document.querySelector("#homeTab");
337
+ if (homeTab && homeTab.isConnected) return homeTab;
338
+
339
+ var indexPage = document.querySelector("#indexPage");
340
+ if (indexPage && indexPage.isConnected) {
341
+ var tab = indexPage.querySelector("#homeTab, .pageTabContent.is-active, .tabContent.is-active");
342
+ if (tab) return tab;
343
+ var sec = indexPage.querySelector(".sections, .homeSectionsContainer");
344
+ if (sec && sec.parentElement) return sec.parentElement;
345
+ return indexPage;
346
  }
347
+
348
+ var activeTab = document.querySelector(".pageTabContent.is-active, div[data-role='page']:not(.hide) .homeSectionsContainer");
349
+ if (activeTab) return activeTab.closest(".pageTabContent, div[data-role='page']") || activeTab;
350
+
351
+ return null;
352
  }
353
 
354
  function installSpotlight() {
355
  var installed = false;
356
  safe(function () {
357
+ var hash = window.location.hash || "";
358
+ if (hash.indexOf("dashboard") !== -1 || hash.indexOf("settings") !== -1 || hash.indexOf("video") !== -1) {
359
  clearSpotlightLifecycle();
360
  return;
361
  }
362
+
363
+ var homeTab = findHomeTab();
364
  if (!homeTab) {
 
365
  return;
366
  }
367
+
368
+ var indexPage = homeTab.closest("#indexPage, div[data-role='page'], .page") || homeTab;
369
+ var favoritesTab = document.querySelector("#favoritesTab");
370
 
371
  installFrameStyle();
372
  installBackdropOverlay();
373
 
374
  var iframe = homeTab.querySelector ? homeTab.querySelector("." + FRAME_CLASS) : null;
375
+ if (!iframe) {
376
+ iframe = document.querySelector("." + FRAME_CLASS);
377
+ }
378
+
379
  if (!iframe) {
380
  iframe = document.createElement("iframe");
381
  iframe.className = FRAME_CLASS + " focusable";
382
  iframe.setAttribute("tabindex", "0");
383
  iframe.src = spotlightUrl;
384
  iframe.title = "Abyss Spotlight";
385
+ iframe.style.width = "100%";
386
+ iframe.style.display = "block";
387
+ iframe.style.border = "none";
388
  iframe.addEventListener("blur", function () { spotlightFocused = false; });
389
+ var sections = homeTab.querySelector ? homeTab.querySelector(".sections, .homeSectionsContainer") : null;
390
+ homeTab.insertBefore(iframe, sections || homeTab.firstChild);
391
+ } else if (iframe.parentElement !== homeTab) {
392
+ var sections = homeTab.querySelector ? homeTab.querySelector(".sections, .homeSectionsContainer") : null;
393
  homeTab.insertBefore(iframe, sections || homeTab.firstChild);
394
  }
395
 
396
+ iframe.style.display = "block";
397
+ iframe.style.visibility = "visible";
398
+ iframe.style.opacity = "1";
399
+ document.documentElement.classList.add("abyss-spotlight-visible");
400
+
401
+ if (indexPage && indexPage.classList) indexPage.classList.add("abyss-spotlight-active");
402
+ if (homeTab.classList) homeTab.classList.add("abyss-spotlight-active");
403
 
404
  if (
405
  indexPage !== currentIndexPage ||
406
  homeTab !== currentHomeTab ||
 
407
  iframe !== currentIframe
408
  ) {
409
  currentIndexPage = indexPage;
 
467
  var logoSvgUrl = (window.location.pathname.indexOf("/web") !== -1) ? "ui/apex-logo.svg" : "/jellyfin/web/ui/apex-logo.svg";
468
 
469
  // 1. Favicon sync
470
+ var links = document.querySelectorAll("a[href*=\x22" + targetId + "\x22], [data-id=\x22" + targetId + "\x22]");
471
  for (var i = 0; i < links.length; i++) {
472
  if (links[i].getAttribute("data-apex-logo") !== "true") {
473
  links[i].setAttribute("data-apex-logo", "true");
 
619
  }, 400);
620
  }
621
 
622
+ var libIds = { movies: "f137a2dd21bbc1b99aa5c0f6bf02a805", tvshows: "767bffe4f11c93ef34b805451a696a4e" };
623
  var librariesDiscovered = false;
624
 
625
  function discoverLibraries() {
 
657
  return;
658
  }
659
 
660
+ var targetId = name === "Movies" ? libIds.movies : libIds.tvshows;
661
+ if (targetId) {
662
+ var links = document.querySelectorAll("a[href*=\x22" + targetId + "\x22], [data-id=\x22" + targetId + "\x22]");
663
+ if (links.length && links[0].click) {
664
+ links[0].click();
 
 
 
 
 
 
 
 
 
 
665
  return;
666
  }
667
+ var targetUrl = "/details?id=" + targetId;
668
+ if (window.Emby && window.Emby.Page && typeof window.Emby.Page.show === "function") {
669
+ window.Emby.Page.show(targetUrl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  return;
671
  }
672
+ window.location.hash = "#" + targetUrl;
673
  return;
674
  }
675
+ window.location.hash = name === "Movies" ? "#/movies" : "#/shows";
676
  }
677
 
678
  function updateActiveNavTab() {
679
  var hash = window.location.hash || "";
680
+ var links = document.querySelectorAll("a[href*=\x22" + targetId + "\x22], [data-id=\x22" + targetId + "\x22]");
681
  for (var i = 0; i < links.length; i++) {
682
  var item = links[i];
683
  var navType = item.getAttribute("data-nav");
 
747
  document.body.appendChild(tvTopbar);
748
  }
749
 
750
+ if (tvTopbar) {
751
+ var isVideo = !!document.querySelector(".videoPlayerContainer:not(.hide), #videoOsdPage:not(.hide), video");
752
+ var hash = window.location.hash || "";
753
+ var isHiddenRoute = hash.indexOf("dashboard") !== -1 || hash.indexOf("settings") !== -1;
754
+ if (isVideo || isHiddenRoute) {
755
+ tvTopbar.style.display = "none";
756
+ } else {
757
+ tvTopbar.style.display = "flex";
758
+ }
759
+ }
760
+
761
  // 2. Hide Hamburger, Cast, SyncPlay, and stray drawer buttons completely
762
  var unwanted = document.querySelectorAll(
763
  'button[aria-label="Menu"], button[aria-label="menu"], ' +