"""Handles /live/browser/* OSC addresses.""" import logging from typing import Optional from .handler import AbletonOSCHandler logger = logging.getLogger(__name__) def _iter_items(node, path_parts, depth=0): """Recursively find a browser item by path.""" if not path_parts: return node name = path_parts[0] try: children = list(node.children) except Exception: return None for child in children: if child.name == name: return _iter_items(child, path_parts[1:], depth + 1) return None class BrowserHandler(AbletonOSCHandler): def init_api(self) -> None: self.clear_listeners() # Category listings (returns flat list of names) for category in [ "audio_effects", "instruments", "midi_effects", "samples", "sounds", "clips", "packs", "plugins", ]: self._add(f"/live/browser/get/{category}", self._make_category_lister(category)) # Load by path (e.g., "Instruments/Wavetable") self._add("/live/browser/load_item", self._load_item) self._add("/live/browser/preview_item", self._preview_item) self._add("/live/browser/stop_preview", self._stop_preview) # Hotswap self._add("/live/browser/get/hotswap_target", self._get_hotswap_target) self._add("/live/browser/begin_hotswap", self._begin_hotswap) # ------------------------------------------------------------------ def _browser(self): try: return self.manager.application().browser except Exception: return None def _make_category_lister(self, category: str): def handler(params: tuple) -> Optional[tuple]: browser = self._browser() if browser is None: return None try: root = getattr(browser, category) names = [item.name for item in root.children] return tuple(names) except Exception as e: logger.warning("browser.%s: %s", category, e) return None return handler def _load_item(self, params: tuple) -> None: """params: path_part1 [, path_part2, ...] -- e.g. 'Instruments' 'Wavetable'""" if not params: return None browser = self._browser() if browser is None: return None path = [str(p) for p in params] # Try to find within all top-level categories for category in [ "audio_effects", "instruments", "midi_effects", "samples", "sounds", "clips", "packs", "plugins", ]: try: root = getattr(browser, category) item = _iter_items(root, path) if item is not None: browser.load_item(item) return None except Exception: continue logger.warning("browser.load_item: item not found: %s", path) return None def _preview_item(self, params: tuple) -> None: if not params: return None browser = self._browser() if browser is None: return None path = [str(p) for p in params] for category in [ "audio_effects", "instruments", "midi_effects", "samples", "sounds", "clips", ]: try: root = getattr(browser, category) item = _iter_items(root, path) if item is not None: browser.preview_item(item) return None except Exception: continue return None def _stop_preview(self, params: tuple) -> None: browser = self._browser() if browser: try: browser.stop_preview() except Exception as e: logger.warning("stop_preview: %s", e) return None def _get_hotswap_target(self, params: tuple) -> Optional[tuple]: browser = self._browser() if browser: try: target = browser.hotswap_target if target is not None: return (target.name,) return ("",) except Exception as e: logger.warning("hotswap_target: %s", e) return None def _begin_hotswap(self, params: tuple) -> None: """params: track_idx, device_idx""" if len(params) < 2: return None try: track_idx = int(params[0]) device_idx = int(params[1]) tracks = list(self.song.tracks) if 0 <= track_idx < len(tracks): devices = list(tracks[track_idx].devices) if 0 <= device_idx < len(devices): browser = self._browser() if browser: browser.hotswap_target = devices[device_idx] except Exception as e: logger.warning("begin_hotswap: %s", e) return None