summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/helper/metadata_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'tidal_dl_ng/helper/metadata_utils.py')
-rw-r--r--tidal_dl_ng/helper/metadata_utils.py227
1 files changed, 227 insertions, 0 deletions
diff --git a/tidal_dl_ng/helper/metadata_utils.py b/tidal_dl_ng/helper/metadata_utils.py
new file mode 100644
index 0000000..eac69bd
--- /dev/null
+++ b/tidal_dl_ng/helper/metadata_utils.py
@@ -0,0 +1,227 @@
+"""Utilities for extracting and formatting metadata from TIDAL objects."""
+
+from contextlib import suppress
+
+
+def _convert_list_to_str(value: list | tuple) -> str:
+ """Convert list/tuple to comma-separated string."""
+ if not value:
+ return "—"
+ with suppress(Exception):
+ return ", ".join([str(x) for x in value])
+ return str(value)
+
+
+def _convert_dict_to_str(value: dict) -> str:
+ """Extract meaningful string from dict."""
+ if "name" in value and value["name"]:
+ return str(value["name"])
+ for k in ("label", "title", "genre", "name"):
+ if k in value and value[k]:
+ return str(value[k])
+ with suppress(Exception):
+ vals = [str(v) for v in value.values() if v is not None]
+ if vals:
+ return ", ".join(vals)
+ return str(value)
+
+
+def safe_str(value: object) -> str:
+ """Convert a potentially non-str value into a safe string for display.
+
+ Args:
+ value: The value to convert.
+
+ Returns:
+ str: Safe string representation, '—' for None/empty.
+ """
+ with suppress(Exception):
+ if value is None:
+ return "—"
+ if isinstance(value, str):
+ return value if value != "" else "—"
+ if isinstance(value, list | tuple):
+ return _convert_list_to_str(value)
+ if isinstance(value, dict):
+ return _convert_dict_to_str(value)
+ return str(value)
+ return "—"
+
+
+def _find_in_dict_container(container: dict, names: tuple[str, ...]) -> object | None:
+ """Search for names in a dict container."""
+ for n in names:
+ if n in container and container[n] is not None:
+ return container[n]
+ # Fuzzy key match
+ keys = list(container.keys())
+ for n in names:
+ for k in keys:
+ if n.lower() in str(k).lower():
+ return container[k]
+ return None
+
+
+def _fuzzy_scan_attrs(obj: object, names: tuple[str, ...]) -> object | None:
+ """Fuzzy scan object attributes for matching names."""
+ with suppress(Exception):
+ for k in dir(obj):
+ kl = k.lower()
+ for n in names:
+ if n.lower() in kl:
+ with suppress(Exception):
+ val = getattr(obj, k)
+ if val is not None:
+ return val
+ return None
+
+
+def find_attr(obj: object, *names: str) -> object | None:
+ """Attempt to find an attribute or data key from an object or its internals.
+
+ Args:
+ obj: The object to inspect.
+ *names: Attribute/key names to search for.
+
+ Returns:
+ The found value or None.
+ """
+ # Direct attributes
+ for n in names:
+ with suppress(Exception):
+ if hasattr(obj, n):
+ val = getattr(obj, n)
+ if val is not None:
+ return val
+
+ # Inspect common dict-like internals
+ for container_name in ("_data", "data", "__dict__"):
+ with suppress(Exception):
+ container = getattr(obj, container_name, None)
+ if isinstance(container, dict):
+ result = _find_in_dict_container(container, names)
+ if result is not None:
+ return result
+
+ # Fuzzy scan of attributes
+ return _fuzzy_scan_attrs(obj, names)
+
+
+def _scan_dict_recursive(container: dict, key_substrings: list[str]) -> object | None:
+ """Recursively scan a dict for keys matching any substring."""
+ for k, v in container.items():
+ kl = str(k).lower()
+ for s in key_substrings:
+ if s.lower() in kl and v is not None:
+ return v
+ # recurse into nested structures
+ if isinstance(v, dict):
+ found = _scan_dict_recursive(v, key_substrings)
+ if found is not None:
+ return found
+ if isinstance(v, list | tuple):
+ for item in v:
+ if isinstance(item, dict):
+ found = _scan_dict_recursive(item, key_substrings)
+ if found is not None:
+ return found
+ return None
+
+
+def search_in_data(obj: object, key_substrings: list[str]) -> object | None:
+ """Recursively search dict-like internals for keys containing any substring.
+
+ Args:
+ obj: The object to search.
+ key_substrings: List of key substrings to search for.
+
+ Returns:
+ The first matching value found, or None.
+ """
+ # check common containers
+ for container_name in ("_data", "data", "__dict__"):
+ with suppress(Exception):
+ container = getattr(obj, container_name, None)
+ if isinstance(container, dict):
+ found = _scan_dict_recursive(container, key_substrings)
+ if found is not None:
+ return found
+
+ # as last resort, try obj.__dict__ if available
+ with suppress(Exception):
+ d = getattr(obj, "__dict__", None)
+ if isinstance(d, dict):
+ return _scan_dict_recursive(d, key_substrings)
+
+ return None
+
+
+def _extract_name_from_dict(item: dict, match_types: tuple[str, ...] | None) -> str | None:
+ """Extract name from a dict if it matches type filters."""
+ if "name" not in item or not item["name"]:
+ return None
+ if match_types:
+ t = item.get("type") or item.get("role") or item.get("credit_type")
+ if t and any(mt in str(t).lower() for mt in match_types):
+ return str(item["name"])
+ return None
+ return str(item["name"])
+
+
+def _extract_name_from_item(item: object) -> str | None:
+ """Extract name from various item formats."""
+ if item is None:
+ return None
+ if isinstance(item, str):
+ return item
+ if isinstance(item, dict):
+ # Try common name keys
+ for k in ("name", "artist", "person"):
+ if k in item and item[k]:
+ return str(item[k])
+ return None
+ # Try object attributes
+ nm = getattr(item, "name", None) or getattr(item, "title", None)
+ if nm:
+ return str(nm)
+ with suppress(Exception):
+ return str(item)
+ return None
+
+
+def extract_names_from_mixed(value: object, match_types: tuple[str, ...] | None = None) -> list[str]:
+ """Normalize various credit-like structures into a list of names.
+
+ Accepts lists of dicts, dicts, strings, or objects.
+ If match_types provided, only include entries where the type/role matches one of them.
+
+ Args:
+ value: The value to extract names from.
+ match_types: Optional tuple of role types to filter by.
+
+ Returns:
+ List of extracted names.
+ """
+ if value is None:
+ return []
+ if isinstance(value, str):
+ return [value]
+ if isinstance(value, dict):
+ name = _extract_name_from_dict(value, match_types)
+ if name:
+ return [name]
+ vals = [str(v) for v in value.values() if v is not None]
+ return vals
+ if isinstance(value, list | tuple):
+ names: list[str] = []
+ for item in value:
+ if isinstance(item, dict):
+ name = _extract_name_from_dict(item, match_types)
+ if name:
+ names.append(name)
+ else:
+ name = _extract_name_from_item(item)
+ if name:
+ names.append(name)
+ return names
+ return []