"""Sound-ID discovery — exclusive to the adaptive discovery_relevant flow.""" from __future__ import annotations import asyncio import re from typing import Any import structlog from playwright.async_api import Error as PlaywrightError from marketing_intelligence.browser.pool import _pool from marketing_intelligence.core.config import settings from marketing_intelligence.evasion.session import get_session from marketing_intelligence.mcp.app import mcp from marketing_intelligence.mcp.tools.guard import tool_guard from marketing_intelligence.mcp.tools.responses import FindSoundIdResponse from marketing_intelligence.scraper.extractor import TikTokExtractor logger = structlog.get_logger("mcp.tools") async def _check_video_for_sound( page: Any, video_url: str, tag_lower: str ) -> str | None: """Navigate to video_url and return the sound_id if music matches tag_lower, else None.""" await page.goto(video_url, wait_until="domcontentloaded", timeout=20000) await asyncio.sleep(2) html = await page.content() blob = TikTokExtractor.state_blob_from_html(html) if not blob: logger.info("find_sound_id.video_no_blob", url=video_url) return None item = ( blob.get("__DEFAULT_SCOPE__", {}) .get("webapp.video-detail", {}) .get("itemInfo", {}) .get("itemStruct", {}) ) if not item: logger.info("find_sound_id.video_no_item", url=video_url) return None music = item.get("music") or {} music_title_norm = re.sub(r"[^a-z0-9]", "", (music.get("title") or "").lower()) logger.info( "find_sound_id.video_check", music_title=music.get("title"), desc=((item.get("desc") or "")[:60]), ) desc = (item.get("desc") or "").lower() challenges = [ (c.get("title") or "").lower() for c in (item.get("challenges") or []) ] matched = ( tag_lower in desc or tag_lower in challenges or tag_lower in music_title_norm or music_title_norm in tag_lower ) if not matched: return None mid = music.get("id") return str(mid) if mid else None @mcp.tool() @tool_guard(FindSoundIdResponse, echo=("artist_handle", "track_tag")) async def find_original_sound_id( artist_handle: str, track_tag: str, session_id: str | None = None, ) -> FindSoundIdResponse: """Find the original TikTok sound_id for a track. Loads artist profile, scrolls to collect video hrefs, then opens each standalone video page and parses SIGI_STATE (which contains full desc/challenges/music on a video page) to find music.original=True + tag match. artist_handle: TikTok handle, e.g. "mylessmithuk" or "@mylessmithuk" track_tag: tag for the track without #, e.g. "hateou" or "feetdontfailmenow" """ proxy = None if session_id: sess = get_session(session_id) if sess: proxy = sess.proxy pool_key = session_id or "__no_session__" page = await _pool.get_page(pool_key, proxy, headless=settings.browser_headless) handle = artist_handle.lstrip("@") try: await page.goto( f"https://www.tiktok.com/@{handle}", wait_until="domcontentloaded", timeout=30000, ) await page.wait_for_selector('a[href*="/video/"]', timeout=20000) for _ in range(3): await page.evaluate("window.scrollTo(0, document.body.scrollHeight)") await asyncio.sleep(2) video_hrefs: list[str] = await page.evaluate("""() => { const seen = new Set(); const results = []; document.querySelectorAll('a[href*="/video/"]').forEach(a => { const href = a.href || a.getAttribute("href") || ""; if (href && !seen.has(href)) { seen.add(href); results.push(href); } }); return results; }""") logger.info( "find_sound_id.profile_video_hrefs", handle=handle, count=len(video_hrefs) ) tag_lower = track_tag.lower().lstrip("#") for video_url in video_hrefs: try: found_sound_id = await _check_video_for_sound( page, video_url, tag_lower ) if found_sound_id: logger.info( "find_sound_id.found_via_video_page", handle=handle, tag=track_tag, sound_id=found_sound_id, video_url=video_url, ) return FindSoundIdResponse( artist_handle=handle, track_tag=track_tag, sound_id=found_sound_id, ) except ( PlaywrightError, asyncio.TimeoutError, AttributeError, KeyError, ValueError, ) as ve: logger.debug( "find_sound_id.video_page_skip", url=video_url, error=str(ve)[:80] ) continue logger.warning( "find_sound_id.not_found", handle=handle, tag=track_tag, checked=len(video_hrefs), ) return FindSoundIdResponse( success=False, artist_handle=handle, track_tag=track_tag, error=f"original sound not found in {len(video_hrefs)} videos of @{handle}", ) except (PlaywrightError, asyncio.TimeoutError) as e: logger.warning( "find_sound_id.profile_videos_error", handle=handle, tag=track_tag, error=str(e)[:200], ) return FindSoundIdResponse( success=False, artist_handle=handle, track_tag=track_tag, error=f"profile video scan failed: {str(e)[:150]}", ) finally: if not session_id: await _pool.close("__no_session__")