Hermes Agent commited on
Commit
b119d32
·
1 Parent(s): 118e711

fix(apex): fix portrait detail title stacking, suppress library playall buttons, and normalize movie title deduplication

Browse files
apex/repair_jellyfin_libraries.py CHANGED
@@ -853,10 +853,10 @@ def heal_missing_db_strm_paths(databases=None):
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",
@@ -866,6 +866,10 @@ def deduplicate_media_strms(roots=None, databases=None):
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)
@@ -891,6 +895,8 @@ def deduplicate_media_strms(roots=None, databases=None):
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:
@@ -930,7 +936,43 @@ def deduplicate_media_strms(roots=None, databases=None):
930
  print(f"[DEDUP] Error removing file {dup}: {e}")
931
  all_deleted_paths.append(dup)
932
 
933
- # Also detect duplicate episodes in the same Season folder matching SxxExx
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
  ep_map = {}
935
  for dirpath, _, filenames in os.walk(root_dir):
936
  for fn in filenames:
@@ -981,33 +1023,41 @@ def deduplicate_media_strms(roots=None, databases=None):
981
  """)
982
  del_count += cur.rowcount
983
 
984
- # Remove duplicate BaseItems rows having identical Name and Type
985
  cur.execute("""
986
- SELECT Name, type, COUNT(*) c
987
  FROM BaseItems
988
  WHERE type LIKE '%Movie' OR type LIKE '%Series'
989
- GROUP BY Name, type
990
- HAVING c > 1
991
  """)
992
- name_dups = cur.fetchall()
993
- for name, itype, _ in name_dups:
994
- cur.execute("SELECT Id, Path FROM BaseItems WHERE Name = ? AND type = ?", (name, itype))
995
- dup_rows = cur.fetchall()
 
 
 
 
 
 
 
996
  def score_db_row(r):
997
- p = (r[1] or "").lower()
 
998
  s = 0
999
- if os.path.exists(r[1] or ""): s += 100
 
 
1000
  if "spider-man" in p: s += 50
1001
  if "shang-chi" in p: s += 50
1002
  if "china-town" in p: s += 50
1003
  if "," in p or "." in p: s += 10
1004
  s += len(p)
1005
  return s
1006
- dup_rows.sort(key=score_db_row, reverse=True)
1007
- for redundant in dup_rows[1:]:
1008
- print(f"[DEDUP] Removing duplicate BaseItems row by name: '{name}' (Id={redundant[0]}, Path={redundant[1]})")
1009
  cur.execute("DELETE FROM BaseItems WHERE Id = ?", (redundant[0],))
1010
- del_count += cur.rowcount
1011
  conn.commit()
1012
  conn.close()
1013
  if del_count > 0:
 
853
 
854
  def deduplicate_media_strms(roots=None, databases=None):
855
  """
856
+ Deduplicates .strm files and folders on disk, and removes duplicate rows from Jellyfin BaseItems.
857
+ When multiple folders or .strm files point to the exact same stream URL, or share the exact same
858
+ normalized movie/show title, picks the canonical version, deletes duplicate folders/files from disk,
859
+ and removes redundant rows from BaseItems in jellyfin.db.
860
  """
861
  media_roots = roots or [
862
  "/data/jellyfin/media/Movies",
 
866
  ]
867
  db_list = databases or TARGET_DATABASES
868
 
869
+ def normalize_title(s: str) -> str:
870
+ cleaned = re.sub(r"\s*\(\s*\d{4}\s*\)\s*", "", s or "")
871
+ return re.sub(r"[^a-z0-9]", "", cleaned.lower())
872
+
873
  def score_path(p: str) -> int:
874
  score = 0
875
  bname = os.path.basename(p)
 
895
  for root_dir in media_roots:
896
  if not os.path.isdir(root_dir):
897
  continue
898
+
899
+ # 1. Deduplicate by identical stream URL inside .strm files
900
  url_map = {}
901
  for dirpath, _, filenames in os.walk(root_dir):
902
  for fn in filenames:
 
936
  print(f"[DEDUP] Error removing file {dup}: {e}")
937
  all_deleted_paths.append(dup)
938
 
939
+ # 2. Deduplicate media folders matching the same normalized title (e.g. Movies/Shang-Chi (2021) vs Movies/Shang-Chi)
940
+ folder_groups = {}
941
+ try:
942
+ for entry in os.scandir(root_dir):
943
+ if entry.is_dir():
944
+ norm_k = normalize_title(entry.name)
945
+ if norm_k:
946
+ folder_groups.setdefault(norm_k, []).append(entry.path)
947
+ except Exception:
948
+ pass
949
+
950
+ for norm_k, fpaths in folder_groups.items():
951
+ if len(fpaths) <= 1:
952
+ continue
953
+ def score_folder(p: str) -> int:
954
+ s = score_path(p)
955
+ try:
956
+ for sub_f in os.listdir(p):
957
+ if sub_f.lower().endswith(".strm") and os.path.getsize(os.path.join(p, sub_f)) > 0:
958
+ s += 100
959
+ break
960
+ except Exception:
961
+ pass
962
+ return s
963
+ fpaths.sort(key=score_folder, reverse=True)
964
+ canon_folder = fpaths[0]
965
+ for dup_folder in fpaths[1:]:
966
+ if dup_folder in all_deleted_dirs:
967
+ continue
968
+ print(f"[DEDUP] Removing duplicate media folder by title: '{dup_folder}' (retaining '{canon_folder}')")
969
+ try:
970
+ shutil.rmtree(dup_folder, ignore_errors=True)
971
+ all_deleted_dirs.append(dup_folder)
972
+ except Exception as e:
973
+ print(f"[DEDUP] Error removing folder {dup_folder}: {e}")
974
+
975
+ # 3. Detect duplicate episodes in the same Season folder matching SxxExx
976
  ep_map = {}
977
  for dirpath, _, filenames in os.walk(root_dir):
978
  for fn in filenames:
 
1023
  """)
1024
  del_count += cur.rowcount
1025
 
1026
+ # Remove duplicate BaseItems rows matching same normalized name and type
1027
  cur.execute("""
1028
+ SELECT Id, Name, Path, type
1029
  FROM BaseItems
1030
  WHERE type LIKE '%Movie' OR type LIKE '%Series'
 
 
1031
  """)
1032
+ all_media_items = cur.fetchall()
1033
+ title_groups = {}
1034
+ for item_id, item_name, item_path, item_type in all_media_items:
1035
+ if not item_name:
1036
+ continue
1037
+ norm_key = (normalize_title(item_name), item_type)
1038
+ title_groups.setdefault(norm_key, []).append((item_id, item_name, item_path))
1039
+
1040
+ for (norm_title, itype), candidates in title_groups.items():
1041
+ if len(candidates) <= 1:
1042
+ continue
1043
  def score_db_row(r):
1044
+ p = (r[2] or "").lower()
1045
+ name = (r[1] or "")
1046
  s = 0
1047
+ if os.path.exists(r[2] or ""): s += 200
1048
+ if re.search(r"\(\d{4}\)", name): s += 30
1049
+ if "-" in name: s += 10
1050
  if "spider-man" in p: s += 50
1051
  if "shang-chi" in p: s += 50
1052
  if "china-town" in p: s += 50
1053
  if "," in p or "." in p: s += 10
1054
  s += len(p)
1055
  return s
1056
+ candidates.sort(key=score_db_row, reverse=True)
1057
+ for redundant in candidates[1:]:
1058
+ print(f"[DEDUP] Removing duplicate BaseItems row by normalized name: '{redundant[1]}' (Id={redundant[0]}, Path={redundant[2]})")
1059
  cur.execute("DELETE FROM BaseItems WHERE Id = ?", (redundant[0],))
1060
+ del_count += 1
1061
  conn.commit()
1062
  conn.close()
1063
  if del_count > 0:
apex/tests/test_repair_jellyfin_libraries.py CHANGED
@@ -756,3 +756,52 @@ def test_deduplicate_media_strms_removes_duplicate_folders_and_db_rows(tmp_path)
756
  assert str(canon_strm) in paths
757
  assert str(dup_strm) not in paths
758
  assert str(dup_dir) not in paths
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  assert str(canon_strm) in paths
757
  assert str(dup_strm) not in paths
758
  assert str(dup_dir) not in paths
759
+
760
+ def test_deduplicate_media_strms_normalizes_title_variants(tmp_path):
761
+ """Ensure duplicate movie folders with differing punctuation, case, or year are deduplicated."""
762
+ movies_root = tmp_path / "Movies"
763
+ movies_root.mkdir()
764
+
765
+ # Folder 1: Spider-Man Far From Home (2019)
766
+ dir1 = movies_root / "Spider-Man Far From Home (2019)"
767
+ dir1.mkdir()
768
+ strm1 = dir1 / "Spider-Man Far From Home (2019).strm"
769
+ strm1.write_text("http://127.0.0.1:8082/stream/111", encoding="utf-8")
770
+
771
+ # Folder 2: Spider-Man: Far from Home
772
+ dir2 = movies_root / "Spider-Man: Far from Home"
773
+ dir2.mkdir()
774
+ strm2 = dir2 / "Spider-Man Far from Home.strm"
775
+ strm2.write_text("http://127.0.0.1:8082/stream/222", encoding="utf-8")
776
+
777
+ # DB with both rows
778
+ db_file = tmp_path / "jellyfin.db"
779
+ conn = sqlite3.connect(str(db_file))
780
+ cur = conn.cursor()
781
+ cur.execute("""
782
+ CREATE TABLE BaseItems (
783
+ Id TEXT PRIMARY KEY,
784
+ Path TEXT,
785
+ Name TEXT,
786
+ type TEXT
787
+ )
788
+ """)
789
+ cur.execute("INSERT INTO BaseItems VALUES ('id_canon', ?, 'Spider-Man: Far From Home', 'MediaBrowser.Controller.Entities.Movies.Movie')", (str(strm1),))
790
+ cur.execute("INSERT INTO BaseItems VALUES ('id_dup', ?, 'Spider-Man: Far from Home', 'MediaBrowser.Controller.Entities.Movies.Movie')", (str(strm2),))
791
+ conn.commit()
792
+ conn.close()
793
+
794
+ deduplicate_media_strms(roots=[str(movies_root)], databases=[str(db_file)])
795
+
796
+ # Exactly one folder and one DB item should remain
797
+ assert dir1.exists() != dir2.exists()
798
+
799
+ conn = sqlite3.connect(str(db_file))
800
+ cur = conn.cursor()
801
+ cur.execute("SELECT Id FROM BaseItems")
802
+ remaining_ids = [r[0] for r in cur.fetchall()]
803
+ conn.close()
804
+
805
+ assert len(remaining_ids) == 1
806
+ assert 'id_canon' in remaining_ids
807
+
apex/ui/abyss.css CHANGED
@@ -3257,9 +3257,11 @@ div[role="main"] {
3257
  /* 9. Action Buttons Across ALL Settings & Dashboard */
3258
  .MuiButton-contained,
3259
  .MuiButton-containedPrimary,
3260
- .type-interior button.raised:not([class*="delete"]):not([class*="cancel"]):not([class*="warn"]):not([class*="restart"]):not([class*="shutdown"]):not([class*="button-flat"]),
3261
- .type-interior .button-submit,
3262
- .settingsContainer button.raised {
 
 
3263
  background: rgba(255, 255, 255, 0.95) !important;
3264
  color: #0e0e11 !important;
3265
  border-radius: 50px !important;
@@ -3278,6 +3280,42 @@ div[role="main"] {
3278
  box-shadow: 0 4px 16px rgba(255, 255, 255, 0.2) !important;
3279
  }
3280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3281
  /* Restart button */
3282
  .MuiButton-root:has([data-testid*="Restart"]),
3283
  button[id*="Restart"],
@@ -4566,78 +4604,89 @@ header .pageTitle,
4566
 
4567
 
4568
  /* ========================================================
4569
- DETAIL PAGE: POSTER THUMBNAIL & METADATA SPACING FIX
4570
- Prevents poster from overlapping title/metadata text
4571
- Ensures title never breaks letter-by-letter in landscape or portrait
4572
  ======================================================== */
4573
- #itemDetailPage .detailRibbon,
4574
- .layout-mobile #itemDetailPage .detailRibbon,
4575
- .layout-desktop #itemDetailPage .detailRibbon,
4576
- .layout-tv #itemDetailPage .detailRibbon,
4577
- .detailRibbon {
4578
- display: flex !important;
4579
- flex-direction: row !important;
4580
- align-items: flex-start !important;
4581
- gap: 20px !important;
4582
- padding-left: 20px !important;
4583
- padding-right: 20px !important;
4584
- padding-top: 16px !important;
4585
- margin-top: 0 !important;
4586
- box-sizing: border-box !important;
4587
- width: 100% !important;
4588
- max-width: 100% !important;
4589
- }
4590
 
4591
- #itemDetailPage .detailImageContainer,
4592
- .detailImageContainer {
4593
- position: relative !important;
4594
- flex: 0 0 150px !important;
4595
- width: 150px !important;
4596
- max-width: 150px !important;
4597
- min-width: 130px !important;
4598
- margin: 0 !important;
4599
- z-index: 2 !important;
4600
- }
4601
 
4602
- #itemDetailPage .detailImageContainer .card,
4603
- .layout-mobile #itemDetailPage .detailImageContainer .card,
4604
- .layout-desktop #itemDetailPage .detailImageContainer .card,
4605
- .layout-tv #itemDetailPage .detailImageContainer .card,
4606
- .detailImageContainer .card {
4607
- position: relative !important;
4608
- top: auto !important;
4609
- left: auto !important;
4610
- margin: 0 !important;
4611
- width: 100% !important;
4612
- max-width: 100% !important;
4613
- border-radius: 12px !important;
4614
- }
4615
 
4616
- #itemDetailPage .detailPagePrimaryContent,
4617
- .layout-mobile #itemDetailPage .detailPagePrimaryContent,
4618
- .layout-desktop #itemDetailPage .detailPagePrimaryContent,
4619
- .layout-tv #itemDetailPage .detailPagePrimaryContent,
4620
- .detailPagePrimaryContent,
4621
- #itemDetailPage .detailPageContent {
4622
- flex: 1 1 auto !important;
4623
- min-width: 0 !important;
4624
- width: auto !important;
4625
- max-width: 100% !important;
4626
- padding-left: 0 !important;
4627
- margin-left: 0 !important;
4628
- }
4629
 
4630
- #itemDetailPage .nameContainer,
4631
- #itemDetailPage .itemName,
4632
- #itemDetailPage .parentName,
4633
- #itemDetailPage h1.itemName,
4634
- .itemName {
4635
- white-space: normal !important;
4636
- word-break: normal !important;
4637
- overflow-wrap: break-word !important;
4638
- display: block !important;
4639
- width: auto !important;
4640
- max-width: 100% !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4641
  }
4642
 
4643
  /* Universal Abyss Spotlight Hero Banner Iframe */
 
3257
  /* 9. Action Buttons Across ALL Settings & Dashboard */
3258
  .MuiButton-contained,
3259
  .MuiButton-containedPrimary,
3260
+ #dashboardPage button.raised:not([class*="delete"]):not([class*="cancel"]):not([class*="warn"]):not([class*="restart"]):not([class*="shutdown"]):not([class*="button-flat"]),
3261
+ .dashboardPage button.raised:not([class*="delete"]):not([class*="cancel"]):not([class*="warn"]):not([class*="restart"]):not([class*="shutdown"]):not([class*="button-flat"]),
3262
+ .settingsContainer button.raised,
3263
+ .formDialog button.raised,
3264
+ .type-interior .button-submit {
3265
  background: rgba(255, 255, 255, 0.95) !important;
3266
  color: #0e0e11 !important;
3267
  border-radius: 50px !important;
 
3280
  box-shadow: 0 4px 16px rgba(255, 255, 255, 0.2) !important;
3281
  }
3282
 
3283
+
3284
+ /* Suppress floating Play All and Shuffle All in library collection views */
3285
+ .page:not(#itemDetailPage):not(#videoOsdPage) .btnPlay,
3286
+ .page:not(#itemDetailPage):not(#videoOsdPage) .btnShuffle,
3287
+ .viewSettings .btnPlay,
3288
+ .viewSettings .btnShuffle,
3289
+ .pageTitleWithDefaultButton .btnPlay,
3290
+ .pageTitleWithDefaultButton .btnShuffle,
3291
+ button[data-action="playall"],
3292
+ button[data-action="shuffleall"],
3293
+ .mainTabsContainer .btnPlay,
3294
+ .mainTabsContainer .btnShuffle,
3295
+ .headerRight .btnPlay,
3296
+ .headerRight .btnShuffle {
3297
+ display: none !important;
3298
+ visibility: hidden !important;
3299
+ width: 0 !important;
3300
+ height: 0 !important;
3301
+ padding: 0 !important;
3302
+ margin: 0 !important;
3303
+ pointer-events: none !important;
3304
+ }
3305
+
3306
+ /* Hidden duplicate cards */
3307
+ .card.apex-duplicate-hidden {
3308
+ display: none !important;
3309
+ visibility: hidden !important;
3310
+ width: 0 !important;
3311
+ height: 0 !important;
3312
+ margin: 0 !important;
3313
+ padding: 0 !important;
3314
+ flex: 0 0 0 !important;
3315
+ pointer-events: none !important;
3316
+ overflow: hidden !important;
3317
+ }
3318
+
3319
  /* Restart button */
3320
  .MuiButton-root:has([data-testid*="Restart"]),
3321
  button[id*="Restart"],
 
4604
 
4605
 
4606
  /* ========================================================
4607
+ DETAIL PAGE: MOBILE PORTRAIT & DEFAULT CLEAN LAYOUT
4608
+ Full-width title, zero text breaking or vertical letter stacking
 
4609
  ======================================================== */
4610
+ @media (orientation: portrait) {
4611
+ #itemDetailPage .detailRibbon,
4612
+ .layout-mobile #itemDetailPage .detailRibbon {
4613
+ display: flex !important;
4614
+ flex-direction: column !important;
4615
+ align-items: stretch !important;
4616
+ width: 100% !important;
4617
+ max-width: 100% !important;
4618
+ padding: 16px 20px !important;
4619
+ gap: 12px !important;
4620
+ box-sizing: border-box !important;
4621
+ }
 
 
 
 
 
4622
 
4623
+ #itemDetailPage .infoWrapper,
4624
+ .layout-mobile #itemDetailPage .infoWrapper {
4625
+ display: flex !important;
4626
+ flex-direction: column !important;
4627
+ align-items: stretch !important;
4628
+ width: 100% !important;
4629
+ max-width: 100% !important;
4630
+ }
 
 
4631
 
4632
+ /* On mobile portrait, hide redundant small poster so full-width backdrop and clean title shine */
4633
+ .layout-mobile #itemDetailPage .detailImageContainer {
4634
+ display: none !important;
4635
+ visibility: hidden !important;
4636
+ width: 0 !important;
4637
+ height: 0 !important;
4638
+ margin: 0 !important;
4639
+ padding: 0 !important;
4640
+ pointer-events: none !important;
4641
+ }
 
 
 
4642
 
4643
+ #itemDetailPage .nameContainer,
4644
+ .layout-mobile #itemDetailPage .nameContainer {
4645
+ display: flex !important;
4646
+ flex-direction: row !important;
4647
+ align-items: center !important;
4648
+ justify-content: space-between !important;
4649
+ width: 100% !important;
4650
+ max-width: 100% !important;
4651
+ margin-bottom: 8px !important;
4652
+ }
 
 
 
4653
 
4654
+ #itemDetailPage .itemName,
4655
+ .layout-mobile #itemDetailPage .itemName,
4656
+ #itemDetailPage h1.itemName {
4657
+ display: block !important;
4658
+ width: 100% !important;
4659
+ max-width: 100% !important;
4660
+ font-size: 1.65rem !important;
4661
+ font-weight: 700 !important;
4662
+ line-height: 1.25 !important;
4663
+ color: #ffffff !important;
4664
+ white-space: normal !important;
4665
+ word-break: normal !important;
4666
+ overflow-wrap: break-word !important;
4667
+ letter-spacing: normal !important;
4668
+ }
4669
+
4670
+ #itemDetailPage .itemMiscInfo,
4671
+ .layout-mobile #itemDetailPage .itemMiscInfo {
4672
+ display: flex !important;
4673
+ flex-direction: row !important;
4674
+ align-items: center !important;
4675
+ flex-wrap: wrap !important;
4676
+ gap: 8px !important;
4677
+ width: 100% !important;
4678
+ margin-bottom: 12px !important;
4679
+ }
4680
+
4681
+ #itemDetailPage .mainDetailButtons,
4682
+ .layout-mobile #itemDetailPage .mainDetailButtons {
4683
+ display: flex !important;
4684
+ flex-direction: row !important;
4685
+ align-items: center !important;
4686
+ flex-wrap: wrap !important;
4687
+ gap: 10px !important;
4688
+ width: 100% !important;
4689
+ }
4690
  }
4691
 
4692
  /* Universal Abyss Spotlight Hero Banner Iframe */
apex/ui/spotlight-loader.js CHANGED
@@ -901,21 +901,28 @@ function findHomeTab() {
901
 
902
  function deduplicateDomCards() {
903
  safe(function () {
904
- var containers = document.querySelectorAll('.itemsContainer');
905
  for (var i = 0; i < containers.length; i++) {
906
  var container = containers[i];
907
- var cards = container.children;
 
908
  var seenTitles = {};
909
  for (var j = 0; j < cards.length; j++) {
910
  var card = cards[j];
911
- if (!card.classList || !card.classList.contains('card')) continue;
912
- var titleEl = card.querySelector('.cardText-first, .cardTextCentered, .cardText, .cardText-secondary');
913
- var title = (titleEl ? titleEl.textContent : (card.getAttribute('aria-label') || '')).trim().toLowerCase();
914
- var normTitle = title.replace(/[^a-z0-9]/g, '');
915
  if (!normTitle) continue;
916
  if (seenTitles[normTitle]) {
917
- card.style.display = 'none';
918
- card.style.visibility = 'hidden';
 
 
 
 
 
 
919
  } else {
920
  seenTitles[normTitle] = true;
921
  }
 
901
 
902
  function deduplicateDomCards() {
903
  safe(function () {
904
+ var containers = document.querySelectorAll(".itemsContainer");
905
  for (var i = 0; i < containers.length; i++) {
906
  var container = containers[i];
907
+ var cards = container.querySelectorAll(":scope > .card, :scope > div > .card");
908
+ if (!cards || !cards.length) cards = container.children;
909
  var seenTitles = {};
910
  for (var j = 0; j < cards.length; j++) {
911
  var card = cards[j];
912
+ if (!card.classList || !card.classList.contains("card")) continue;
913
+ var titleEl = card.querySelector(".cardText-first") || card.querySelector(".cardText:not(.cardText-secondary)") || card.querySelector(".cardTextCentered") || card.querySelector(".cardText");
914
+ var rawTitle = (titleEl ? titleEl.textContent : (card.getAttribute("aria-label") || "")).trim().toLowerCase();
915
+ var normTitle = rawTitle.replace(/\s*\(\s*\d{4}\s*\)\s*/g, "").replace(/[^a-z0-9]/g, "");
916
  if (!normTitle) continue;
917
  if (seenTitles[normTitle]) {
918
+ card.classList.add("apex-duplicate-hidden");
919
+ card.style.setProperty("display", "none", "important");
920
+ card.style.setProperty("visibility", "hidden", "important");
921
+ card.style.setProperty("width", "0px", "important");
922
+ card.style.setProperty("height", "0px", "important");
923
+ card.style.setProperty("flex", "0 0 0px", "important");
924
+ card.style.setProperty("margin", "0px", "important");
925
+ card.style.setProperty("padding", "0px", "important");
926
  } else {
927
  seenTitles[normTitle] = true;
928
  }