/** * TikTok WebView Injection Script * * Extracts data available to the logged-in user from TikTok web using DOM and * hydration-state scraping. TikTok's public web surface changes often, so the * script intentionally combines several low-risk sources: * - embedded hydration JSON * - visible profile DOM * - visible video cards on profile/liked tabs */ export declare const TIKTOK_EXPORT_SCRIPT = "\n(async function() {\n try {\n var postMsg = function(obj) {\n window.ReactNativeWebView.postMessage(JSON.stringify(obj));\n };\n\n var progress = function(stage, pct, msg) {\n postMsg({ type: 'TIKTOK_EXPORT_PROGRESS', stage: stage, progress: pct, message: msg });\n };\n\n var debug = function(step, details) {\n postMsg({\n type: 'TIKTOK_EXPORT_DEBUG',\n step: step,\n details: details || {},\n timestamp: Date.now()\n });\n };\n\n function delay(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }\n\n function text(selector, root) {\n var el = (root || document).querySelector(selector);\n return el && el.textContent ? el.textContent.trim() : '';\n }\n\n function attr(selector, name, root) {\n var el = (root || document).querySelector(selector);\n return el ? (el.getAttribute(name) || '') : '';\n }\n\n function cleanNumber(value) {\n if (typeof value === 'number') return value;\n if (!value || typeof value !== 'string') return undefined;\n var normalized = value.replace(/,/g, '').trim().toUpperCase();\n var multiplier = 1;\n if (normalized.endsWith('K')) multiplier = 1000;\n if (normalized.endsWith('M')) multiplier = 1000000;\n if (normalized.endsWith('B')) multiplier = 1000000000;\n var parsed = parseFloat(normalized.replace(/[^0-9.]/g, ''));\n return Number.isFinite(parsed) ? Math.round(parsed * multiplier) : undefined;\n }\n\n function findByKeys(root, predicate, limit) {\n var results = [];\n var seen = [];\n function visit(node) {\n if (!node || results.length >= limit) return;\n if (typeof node !== 'object') return;\n if (seen.indexOf(node) !== -1) return;\n seen.push(node);\n try {\n if (predicate(node)) results.push(node);\n if (Array.isArray(node)) {\n for (var i = 0; i < node.length; i++) visit(node[i]);\n return;\n }\n var keys = Object.keys(node);\n for (var k = 0; k < keys.length; k++) visit(node[keys[k]]);\n } catch(e) {}\n }\n visit(root);\n return results;\n }\n\n function parseHydrationStates(doc) {\n var states = [];\n var scripts = (doc || document).querySelectorAll('script');\n for (var i = 0; i < scripts.length; i++) {\n var id = scripts[i].id || '';\n var raw = scripts[i].textContent || '';\n if (!raw || raw.length < 20) continue;\n if (id.indexOf('SIGI_STATE') === -1 && id.indexOf('UNIVERSAL_DATA') === -1 && raw.indexOf('\"ItemModule\"') === -1 && raw.indexOf('\"UserModule\"') === -1) continue;\n try {\n states.push(JSON.parse(raw));\n } catch(e) {}\n }\n return states;\n }\n\n function normalizeVideo(raw, fallbackAuthor, source) {\n if (!raw || typeof raw !== 'object') return null;\n var id = raw.id || raw.videoId || raw.aweme_id || raw.item_id || raw.group_id;\n var desc = raw.desc || raw.description || raw.title || raw.caption || (raw.contents && raw.contents[0] && raw.contents[0].desc) || '';\n var author = raw.author || raw.authorInfo || raw.authorStats || {};\n var authorUsername = raw.authorId || raw.authorUniqueId || author.uniqueId || author.username || fallbackAuthor || '';\n var stats = raw.stats || raw.statsV2 || raw.statistics || {};\n var covers = raw.video || raw.covers || {};\n var music = raw.music || {};\n var url = id ? ('https://www.tiktok.com/@' + encodeURIComponent(authorUsername || 'user') + '/video/' + encodeURIComponent(String(id))) : '';\n var hashtags = [];\n if (Array.isArray(raw.textExtra)) {\n hashtags = raw.textExtra.map(function(tag) { return tag && (tag.hashtagName || tag.name); }).filter(Boolean);\n } else if (desc) {\n hashtags = (desc.match(/#[\\w.-]+/g) || []).map(function(tag) { return tag.slice(1); });\n }\n if (!id && !desc && !url) return null;\n return {\n videoId: id ? String(id) : url || desc.slice(0, 80),\n description: desc || undefined,\n caption: desc || undefined,\n authorUsername: authorUsername || undefined,\n authorDisplayName: author.nickname || author.displayName || undefined,\n createdAt: raw.createTime ? new Date(Number(raw.createTime) * 1000).toISOString() : undefined,\n url: url || raw.shareUrl || raw.webVideoUrl || undefined,\n thumbnailUrl: covers.cover || covers.originCover || covers.dynamicCover || raw.cover || undefined,\n musicTitle: music.title || undefined,\n musicAuthor: music.authorName || music.author || undefined,\n stats: {\n likes: cleanNumber(stats.diggCount || stats.digg_count || stats.likeCount || stats.likes),\n comments: cleanNumber(stats.commentCount || stats.comment_count || stats.comments),\n shares: cleanNumber(stats.shareCount || stats.share_count || stats.shares),\n views: cleanNumber(stats.playCount || stats.play_count || stats.views),\n },\n hashtags: hashtags,\n source: source,\n };\n }\n\n function normalizeUser(raw) {\n if (!raw || typeof raw !== 'object') return null;\n var user = raw.user || raw.userInfo || raw;\n var stats = raw.stats || raw.statsV2 || user.stats || {};\n var username = user.uniqueId || user.username || user.secUid || '';\n if (!username && !user.nickname && !user.signature) return null;\n return {\n userId: user.id || user.uid || user.secUid || undefined,\n username: username || undefined,\n displayName: user.nickname || user.displayName || undefined,\n bio: user.signature || user.bio || undefined,\n avatarUrl: user.avatarLarger || user.avatarMedium || user.avatarThumb || undefined,\n profileUrl: username ? ('https://www.tiktok.com/@' + username) : undefined,\n stats: {\n following: cleanNumber(stats.followingCount || stats.following),\n followers: cleanNumber(stats.followerCount || stats.followers),\n likes: cleanNumber(stats.heartCount || stats.diggCount || stats.likes),\n videos: cleanNumber(stats.videoCount || stats.videos),\n },\n verified: !!user.verified,\n privateAccount: !!user.privateAccount,\n };\n }\n\n function extractFromStates(states, fallbackAuthor, source) {\n var videos = [];\n var users = [];\n var seenVideos = {};\n states.forEach(function(state) {\n var candidates = findByKeys(state, function(node) {\n return !!(node && (node.desc || node.description || node.video || node.stats) && (node.id || node.videoId || node.aweme_id || node.item_id));\n }, 120);\n candidates.forEach(function(item) {\n var video = normalizeVideo(item, fallbackAuthor, source);\n if (video && !seenVideos[video.videoId]) {\n seenVideos[video.videoId] = true;\n videos.push(video);\n }\n });\n\n var userCandidates = findByKeys(state, function(node) {\n return !!(node && (node.uniqueId || node.username || node.signature) && (node.nickname || node.avatarThumb || node.stats));\n }, 20);\n userCandidates.forEach(function(item) {\n var user = normalizeUser(item);\n if (user) users.push(user);\n });\n });\n return { videos: videos, users: users };\n }\n\n function extractProfileFromDom() {\n var urlMatch = location.pathname.match(/@([^/?]+)/);\n var username = urlMatch ? decodeURIComponent(urlMatch[1]) : '';\n var displayName = text('[data-e2e=\"user-title\"]') || text('h1');\n var bio = text('[data-e2e=\"user-bio\"]') || text('[data-e2e=\"user-signature\"]');\n var followers = text('[data-e2e=\"followers-count\"]');\n var following = text('[data-e2e=\"following-count\"]');\n var likes = text('[data-e2e=\"likes-count\"]');\n var avatarUrl = attr('img[alt*=\"avatar\" i], [data-e2e=\"user-avatar\"] img', 'src');\n if (!username && !displayName && !bio) return null;\n return {\n username: username || undefined,\n displayName: displayName || undefined,\n bio: bio || undefined,\n avatarUrl: avatarUrl || undefined,\n profileUrl: username ? ('https://www.tiktok.com/@' + username) : location.href,\n stats: {\n following: cleanNumber(following),\n followers: cleanNumber(followers),\n likes: cleanNumber(likes),\n },\n };\n }\n\n function extractVideosFromDom(source, fallbackAuthor, root) {\n var seen = {};\n var videos = [];\n var scope = root || document;\n var anchors = scope.querySelectorAll('a[href*=\"/video/\"]');\n for (var i = 0; i < anchors.length; i++) {\n var href = anchors[i].href || anchors[i].getAttribute('href') || '';\n var match = href.match(/\\/video\\/(\\d+)/);\n var authorMatch = href.match(/\\/(@[^/]+)\\/video\\//);\n var card = anchors[i].closest('div') || anchors[i];\n var img = card.querySelector('img');\n var desc = anchors[i].getAttribute('title') || (img && img.getAttribute('alt')) || text('[data-e2e=\"video-desc\"]', card);\n var views = text('[data-e2e=\"video-views\"]', card) || text('strong', card);\n var id = match ? match[1] : href;\n if (!id || seen[id]) continue;\n seen[id] = true;\n videos.push({\n videoId: id,\n description: desc || undefined,\n caption: desc || undefined,\n authorUsername: authorMatch ? decodeURIComponent(authorMatch[1].replace(/^@/, '')) : (fallbackAuthor || undefined),\n url: href || undefined,\n thumbnailUrl: img ? (img.src || img.getAttribute('src') || undefined) : undefined,\n stats: { views: cleanNumber(views) },\n hashtags: desc ? ((desc.match(/#[\\w.-]+/g) || []).map(function(tag) { return tag.slice(1); })) : [],\n source: source,\n });\n }\n return videos;\n }\n\n function findLikedVideoContainer() {\n var selectors = [\n '[data-e2e=\"user-liked-item-list\"]',\n '[data-e2e=\"liked-item-list\"]',\n '[data-e2e=\"user-liked-list\"]',\n '[data-e2e*=\"liked\"][data-e2e*=\"list\"]',\n '[data-e2e*=\"Liked\"][data-e2e*=\"list\"]'\n ];\n\n for (var i = 0; i < selectors.length; i++) {\n try {\n var container = document.querySelector(selectors[i]);\n if (container && container.querySelector('a[href*=\"/video/\"]')) {\n return container;\n }\n } catch(e) {}\n }\n\n return null;\n }\n\n function linksLookLikeOwnPosts(videos, username) {\n if (!videos.length || !username) return false;\n var ownCount = videos.filter(function(video) {\n return video && video.authorUsername && video.authorUsername.toLowerCase() === username.toLowerCase();\n }).length;\n return ownCount === videos.length;\n }\n\n function isVisibleElement(element) {\n try {\n var rect = element && element.getBoundingClientRect ? element.getBoundingClientRect() : null;\n var style = element && window.getComputedStyle ? window.getComputedStyle(element) : null;\n return !!(\n rect &&\n rect.width > 0 &&\n rect.height > 0 &&\n rect.bottom > 0 &&\n rect.right > 0 &&\n rect.top < window.innerHeight &&\n rect.left < window.innerWidth &&\n (!style || (style.visibility !== 'hidden' && style.display !== 'none' && style.opacity !== '0'))\n );\n } catch(e) {\n return false;\n }\n }\n\n function getElementSignature(element) {\n if (!element) return '';\n try {\n return [\n element.tagName ? element.tagName.toLowerCase() : '',\n element.getAttribute && (element.getAttribute('role') || ''),\n element.getAttribute && (element.getAttribute('data-e2e') || ''),\n element.getAttribute && (element.getAttribute('aria-label') || ''),\n (element.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 80)\n ].filter(Boolean).join(' | ');\n } catch(e) {\n return '';\n }\n }\n\n function toRectString(rect) {\n if (!rect) return '';\n return Math.round(rect.left) + ',' + Math.round(rect.top) + ',' + Math.round(rect.width) + 'x' + Math.round(rect.height);\n }\n\n function isClickableElement(el) {\n if (!el) return false;\n var role = (el.getAttribute && el.getAttribute('role')) || '';\n var tabIndex = el.getAttribute && el.getAttribute('tabindex');\n var tag = el.tagName ? el.tagName.toLowerCase() : '';\n var hasInlineOnClick = !!(el.getAttribute && el.getAttribute('onclick'));\n return role === 'tab' || role === 'button' || tabIndex != null || tag === 'button' || tag === 'a' || hasInlineOnClick;\n }\n\n function findClickableTabAncestor(element) {\n var current = element;\n var best = element;\n var depth = 0;\n while (current && depth < 8) {\n var role = (current.getAttribute && current.getAttribute('role')) || '';\n var tabIndex = current.getAttribute && current.getAttribute('tabindex');\n var tag = current.tagName ? current.tagName.toLowerCase() : '';\n var textValue = (current.textContent || '').trim().replace(/\\s+/g, ' ').toLowerCase();\n var rect = current.getBoundingClientRect ? current.getBoundingClientRect() : null;\n var compactTabText = textValue === 'liked' || textValue === 'likes';\n var looksClickable = role === 'tab' || role === 'button' || tabIndex != null || tag === 'button' || tag === 'a' || typeof current.onclick === 'function';\n var staysInTabSize = rect && rect.width <= window.innerWidth && rect.height <= 90;\n\n if (isVisibleElement(current) && compactTabText && (looksClickable || staysInTabSize)) {\n best = current;\n }\n\n if (isVisibleElement(current) && looksClickable && (compactTabText || depth > 0)) {\n return current;\n }\n\n current = current.parentElement;\n depth++;\n }\n return best;\n }\n\n function findLikedSpanTabButton() {\n try {\n var spans = Array.prototype.slice.call(document.querySelectorAll('span'));\n var firstGridTop = getFirstVideoGridTop();\n var visibleLikedSpans = spans.filter(function(span) {\n var value = (span.textContent || '').trim().toLowerCase();\n if (value !== 'liked') return false;\n if (!isVisibleElement(span)) return false;\n var dataE2e = span.getAttribute && (span.getAttribute('data-e2e') || '').toLowerCase();\n if (dataE2e === 'likes' || dataE2e === 'user-likes') return false;\n var rect = span.getBoundingClientRect();\n // The real profile tab is above the video grid; captions and other page text are not.\n return !firstGridTop || rect.top < firstGridTop;\n });\n\n if (visibleLikedSpans.length > 0) {\n visibleLikedSpans.sort(function(a, b) {\n var ar = a.getBoundingClientRect();\n var br = b.getBoundingClientRect();\n return (br.left - ar.left) || (ar.top - br.top);\n });\n return findClickableTabAncestor(visibleLikedSpans[0]);\n }\n } catch(e) {}\n\n return null;\n }\n\n function detectProfileTabRow(firstGridTop) {\n var selectors = '[role=\"tab\"], button, a, div[tabindex], span[tabindex], div, span';\n var rowTopLimit = firstGridTop ? Math.min(firstGridTop + 12, window.innerHeight - 40) : Math.round(window.innerHeight * 0.72);\n var rowBottomFloor = 40;\n var candidates = [];\n try {\n var nodes = Array.prototype.slice.call(document.querySelectorAll(selectors));\n for (var i = 0; i < nodes.length; i++) {\n var el = nodes[i];\n if (!el || !isVisibleElement(el)) continue;\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!rect) continue;\n if (rect.top < rowBottomFloor || rect.top > rowTopLimit) continue;\n if (rect.height < 14 || rect.height > 70) continue;\n if (rect.width < 20 || rect.width > Math.max(200, window.innerWidth * 0.75)) continue;\n var textValue = (el.textContent || '').trim().replace(/s+/g, ' ');\n var textLower = textValue.toLowerCase();\n if (textValue.length > 60) continue;\n if (textLower === 'following' || textLower === 'followers' || textLower === 'likes') continue;\n var hasIcon = !!el.querySelector && !!el.querySelector('svg, path, img');\n if (!isClickableElement(el) && !hasIcon && textValue.length === 0) continue;\n candidates.push({ el: el, rect: rect, text: textLower, hasIcon: hasIcon });\n }\n } catch(e) {}\n\n var byRow = {};\n for (var j = 0; j < candidates.length; j++) {\n var centerY = candidates[j].rect.top + (candidates[j].rect.height / 2);\n var key = String(Math.round(centerY / 8) * 8);\n if (!byRow[key]) byRow[key] = [];\n byRow[key].push(candidates[j]);\n }\n\n var bestRow = null;\n var bestScore = -1;\n var keys = Object.keys(byRow);\n for (var k = 0; k < keys.length; k++) {\n var row = byRow[keys[k]].sort(function(a, b) { return a.rect.left - b.rect.left; });\n var spread = row.length ? (row[row.length - 1].rect.right - row[0].rect.left) : 0;\n var uniqueXBuckets = {};\n for (var r = 0; r < row.length; r++) uniqueXBuckets[String(Math.round(row[r].rect.left / 24))] = true;\n var uniqueCount = Object.keys(uniqueXBuckets).length;\n if (uniqueCount < 3 || spread < window.innerWidth * 0.45) continue;\n var statsWordCount = row.filter(function(entry) {\n var text = (entry.text || '').toLowerCase();\n if (!text) return false;\n return (\n text === 'following' ||\n text === 'followers' ||\n text === 'likes' ||\n /d+s*following$/.test(text) ||\n /d+s*followers$/.test(text) ||\n /d+s*likes$/.test(text)\n );\n }).length;\n if (statsWordCount >= 2) continue;\n var iconCount = row.filter(function(entry) { return entry.hasIcon; }).length;\n var rowY = parseInt(keys[k], 10);\n var expectedTabY = firstGridTop ? Math.max(80, firstGridTop - 108) : Math.round(window.innerHeight * 0.28);\n var closeness = firstGridTop ? (1000 - Math.abs(firstGridTop - rowY)) : 600;\n var tabOffsetCloseness = 1000 - Math.abs(expectedTabY - rowY);\n var countBoost = uniqueCount >= 4 ? 520 : (uniqueCount === 3 ? 80 : 0);\n var score = closeness + tabOffsetCloseness + countBoost + (uniqueCount * 30) + (iconCount * 24) + Math.min(240, spread);\n if (score > bestScore) {\n bestScore = score;\n bestRow = row;\n }\n }\n\n if (!bestRow) {\n return null;\n }\n\n return {\n rowY: Math.round(bestRow[0].rect.top + (bestRow[0].rect.height / 2)),\n elements: bestRow\n };\n }\n\n function isStatsLikeText(text) {\n var t = String(text || '').toLowerCase().replace(/s+/g, '');\n if (!t) return false;\n if (t === 'following' || t === 'followers' || t === 'likes') return true;\n if (t.indexOf('following') !== -1 || t.indexOf('followers') !== -1 || t.indexOf('likes') !== -1) {\n return /d/.test(t) || t === 'following' || t === 'followers' || t === 'likes';\n }\n return false;\n }\n\n function isStatsLikeRow(rowCandidate) {\n if (!rowCandidate || !rowCandidate.elements || !rowCandidate.elements.length) return false;\n var statsCount = 0;\n for (var i = 0; i < rowCandidate.elements.length; i++) {\n var text = rowCandidate.elements[i] && rowCandidate.elements[i].text;\n if (isStatsLikeText(text)) statsCount++;\n }\n return statsCount >= 2;\n }\n\n function isStatsLikeRowBySignature(rowCandidate) {\n if (!rowCandidate || !rowCandidate.elements || !rowCandidate.elements.length) return false;\n var statsMatches = 0;\n for (var i = 0; i < rowCandidate.elements.length; i++) {\n var entry = rowCandidate.elements[i];\n var sig = getElementSignature(entry && entry.el).toLowerCase();\n if (!sig) continue;\n if (/(d+s*)?(following|followers|likes)\b/.test(sig)) {\n statsMatches++;\n }\n }\n return statsMatches >= 2;\n }\n\n function isLikelyTabCellRow(rowCandidate, firstGridTop) {\n if (!rowCandidate || !rowCandidate.elements || rowCandidate.elements.length < 4) return false;\n var row = rowCandidate.elements;\n var rowY = rowCandidate.rowY || 0;\n if (firstGridTop) {\n var minExpected = Math.max(56, firstGridTop - 170);\n var maxExpected = Math.max(minExpected + 16, firstGridTop - 26);\n if (rowY < minExpected || rowY > maxExpected) return false;\n }\n\n var shortOrEmptyTextCount = 0;\n var narrowCellCount = 0;\n var widthValues = [];\n for (var i = 0; i < row.length; i++) {\n var entry = row[i];\n var text = (entry && entry.text ? entry.text : '').trim();\n var rect = entry && entry.rect;\n if (text.length <= 12) shortOrEmptyTextCount++;\n if (rect && rect.width >= 48 && rect.width <= 100 && rect.height >= 30 && rect.height <= 68) {\n narrowCellCount++;\n widthValues.push(rect.width);\n }\n }\n var avgWidth = widthValues.length ? widthValues.reduce(function(sum, width) { return sum + width; }, 0) / widthValues.length : 0;\n var widthVarianceCount = widthValues.filter(function(width) { return Math.abs(width - avgWidth) <= 16; }).length;\n return (\n narrowCellCount >= Math.max(3, row.length - 1) &&\n widthVarianceCount >= Math.max(3, row.length - 1) &&\n (shortOrEmptyTextCount >= Math.max(2, row.length - 2))\n );\n }\n\n function detectAnonymousTabCellRow(firstGridTop) {\n if (!firstGridTop) return null;\n var minY = Math.max(64, firstGridTop - 170);\n var maxY = Math.max(minY + 20, firstGridTop - 18);\n var candidates = [];\n try {\n var els = Array.prototype.slice.call(document.querySelectorAll('div, span, button, [role=\"tab\"], [tabindex]'));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (!el || !isVisibleElement(el)) continue;\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!rect) continue;\n if (rect.top < minY || rect.top > maxY) continue;\n if (rect.width < 52 || rect.width > 96) continue;\n if (rect.height < 30 || rect.height > 72) continue;\n var txt = (el.textContent || '').trim().replace(/s+/g, ' ').toLowerCase();\n if (isStatsLikeText(txt)) continue;\n candidates.push({ el: el, rect: rect, text: txt, hasIcon: !!el.querySelector && !!el.querySelector('svg, path, img') });\n }\n } catch(e) {}\n\n var rows = {};\n for (var j = 0; j < candidates.length; j++) {\n var key = String(Math.round((candidates[j].rect.top + candidates[j].rect.height / 2) / 6) * 6);\n if (!rows[key]) rows[key] = [];\n rows[key].push(candidates[j]);\n }\n\n var best = null;\n var bestScore = -1;\n var keys = Object.keys(rows);\n for (var k = 0; k < keys.length; k++) {\n var row = rows[keys[k]].sort(function(a, b) { return a.rect.left - b.rect.left; });\n if (row.length < 4) continue;\n var spread = row[row.length - 1].rect.right - row[0].rect.left;\n if (spread < window.innerWidth * 0.58) continue;\n var rowY = parseInt(keys[k], 10);\n var expectedY = Math.max(68, firstGridTop - 108);\n var closeness = 1000 - Math.abs(expectedY - rowY);\n var score = closeness + (row.length * 120) + Math.min(220, spread);\n if (score > bestScore) {\n bestScore = score;\n best = row;\n }\n }\n\n if (!best) return null;\n return {\n rowY: Math.round(best[0].rect.top + best[0].rect.height / 2),\n elements: best\n };\n }\n\n function detectIconTabRowNearGrid(firstGridTop) {\n if (!firstGridTop) return null;\n var minY = Math.max(72, firstGridTop - 96);\n var maxY = Math.max(minY + 24, firstGridTop - 12);\n var nodes = [];\n try {\n var els = Array.prototype.slice.call(document.querySelectorAll('button, [role=\"tab\"], div, span, a'));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (!el || !isVisibleElement(el)) continue;\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!rect) continue;\n if (rect.top < minY || rect.top > maxY) continue;\n if (rect.width < 28 || rect.width > 130) continue;\n if (rect.height < 20 || rect.height > 72) continue;\n var text = (el.textContent || '').trim().replace(/s+/g, ' ');\n if (text.length > 28) continue;\n if (isStatsLikeText(text)) continue;\n var hasIcon = !!el.querySelector && !!el.querySelector('svg, path, img');\n var clickable = isClickableElement(el);\n if (!hasIcon && !clickable && !text) continue;\n nodes.push({ el: el, rect: rect, text: text.toLowerCase(), hasIcon: hasIcon });\n }\n } catch(e) {}\n\n var byRow = {};\n for (var j = 0; j < nodes.length; j++) {\n var yKey = String(Math.round((nodes[j].rect.top + nodes[j].rect.height / 2) / 6) * 6);\n if (!byRow[yKey]) byRow[yKey] = [];\n byRow[yKey].push(nodes[j]);\n }\n\n var best = null;\n var bestScore = -1;\n var keys = Object.keys(byRow);\n for (var k = 0; k < keys.length; k++) {\n var row = byRow[keys[k]].sort(function(a, b) { return a.rect.left - b.rect.left; });\n if (row.length < 3) continue;\n var spread = row[row.length - 1].rect.right - row[0].rect.left;\n if (spread < window.innerWidth * 0.5) continue;\n var iconCount = row.filter(function(entry) { return entry.hasIcon; }).length;\n var statsCount = row.filter(function(entry) { return isStatsLikeText(entry.text); }).length;\n if (statsCount > 0) continue;\n var rowY = parseInt(keys[k], 10);\n var expected = Math.max(64, firstGridTop - 48);\n var score = (1000 - Math.abs(expected - rowY)) + (row.length * 90) + (iconCount * 60) + Math.min(260, spread);\n if (score > bestScore) {\n bestScore = score;\n best = row;\n }\n }\n\n if (!best) return null;\n return {\n rowY: Math.round(best[0].rect.top + best[0].rect.height / 2),\n elements: best\n };\n }\n\n function findLikedTabButton() {\n var spanTab = findLikedSpanTabButton();\n if (spanTab) return spanTab;\n\n var explicitSelectors = [\n '[data-e2e=\"liked-tab\"]',\n '[data-e2e=\"user-liked-tab\"]',\n '[data-e2e=\"profile-liked-tab\"]',\n '[data-e2e*=\"liked\"][role=\"tab\"]',\n '[data-e2e*=\"Liked\"][role=\"tab\"]',\n '[aria-label*=\"liked\" i][role=\"tab\"]',\n '[aria-label*=\"liked\" i][role=\"button\"]'\n ];\n\n for (var si = 0; si < explicitSelectors.length; si++) {\n try {\n var explicit = document.querySelector(explicitSelectors[si]);\n if (explicit) return explicit;\n } catch(e) {}\n }\n\n try {\n var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {\n acceptNode: function(node) {\n var value = (node && node.nodeValue ? node.nodeValue : '').trim().toLowerCase();\n return value === 'liked'\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n }\n });\n var textNode = walker.nextNode();\n while (textNode) {\n var element = textNode.parentElement;\n var depth = 0;\n while (element && depth < 8) {\n var elementText = (element.textContent || '').trim().toLowerCase();\n var role = (element.getAttribute && element.getAttribute('role')) || '';\n var tabIndex = element.getAttribute && element.getAttribute('tabindex');\n var tag = element.tagName ? element.tagName.toLowerCase() : '';\n var clickable = role === 'tab' || role === 'button' || tabIndex != null || tag === 'button' || tag === 'a' || typeof element.onclick === 'function';\n var exactOrCompact = elementText === 'liked' || elementText.replace(/\\s+/g, ' ').indexOf('liked') !== -1;\n if (isVisibleElement(element) && exactOrCompact && (clickable || depth >= 1)) {\n return element;\n }\n element = element.parentElement;\n depth++;\n }\n textNode = walker.nextNode();\n }\n } catch(e) {}\n\n var candidates = Array.prototype.slice.call(document.querySelectorAll('[role=\"tab\"], button, div[tabindex], span[tabindex], div, span'));\n return candidates.find(function(el) {\n var label = [\n el.getAttribute('aria-label') || '',\n el.getAttribute('title') || '',\n el.getAttribute('data-e2e') || '',\n el.textContent || ''\n ].join(' ').trim().toLowerCase();\n var inTabArea = !!el.closest('[role=\"tablist\"], [data-e2e*=\"profile\"], main');\n if (!inTabArea) return false;\n if (label === 'liked') return true;\n if (label.indexOf('liked videos') !== -1) return true;\n if (label.indexOf(' liked') !== -1 && label.indexOf('favorites') !== -1) return true;\n return false;\n });\n }\n\n function triggerUserClick(element) {\n if (!element) return false;\n try {\n if (!isVisibleElement(element)) {\n element.scrollIntoView({ block: 'center', inline: 'center' });\n }\n } catch(e) {}\n\n try {\n var rect = element.getBoundingClientRect ? element.getBoundingClientRect() : null;\n var x = rect ? rect.left + rect.width / 2 : 0;\n var y = rect ? rect.top + rect.height / 2 : 0;\n var pointTarget = rect ? document.elementFromPoint(x, y) : element;\n var pointIsGenericRoot = pointTarget === document.documentElement || pointTarget === document.body;\n var target = (!pointTarget || pointIsGenericRoot) ? element : pointTarget;\n ['pointerdown', 'mousedown', 'mouseup', 'pointerup', 'click'].forEach(function(type) {\n var EventCtor = type.indexOf('pointer') === 0 && typeof PointerEvent !== 'undefined' ? PointerEvent : MouseEvent;\n var eventObj = new EventCtor(type, {\n bubbles: true,\n cancelable: true,\n clientX: x,\n clientY: y,\n pointerType: 'touch'\n });\n try { element.dispatchEvent(eventObj); } catch(e) {}\n if (target !== element) {\n try {\n target.dispatchEvent(new EventCtor(type, {\n bubbles: true,\n cancelable: true,\n clientX: x,\n clientY: y,\n pointerType: 'touch'\n }));\n } catch(e) {}\n }\n });\n if (typeof element.click === 'function') {\n element.click();\n }\n return true;\n } catch(e) {\n try {\n element.click();\n return true;\n } catch(_e) {\n return false;\n }\n }\n }\n\n function dispatchTapAt(x, y) {\n try {\n var target = document.elementFromPoint(x, y);\n if (!target) return false;\n ['touchstart', 'pointerdown', 'mousedown', 'mouseup', 'pointerup', 'touchend', 'click'].forEach(function(type) {\n var EventCtor = type.indexOf('pointer') === 0 && typeof PointerEvent !== 'undefined' ? PointerEvent : MouseEvent;\n var eventOptions = {\n bubbles: true,\n cancelable: true,\n clientX: x,\n clientY: y,\n pointerType: 'touch'\n };\n if (type.indexOf('touch') === 0 && typeof TouchEvent !== 'undefined') {\n target.dispatchEvent(new TouchEvent(type, { bubbles: true, cancelable: true }));\n } else {\n target.dispatchEvent(new EventCtor(type, eventOptions));\n }\n });\n if (target && typeof target.click === 'function') {\n target.click();\n }\n return true;\n } catch(e) {\n return false;\n }\n }\n\n function getFirstVideoGridTop() {\n var anchors = Array.prototype.slice.call(document.querySelectorAll('a[href*=\"/video/\"]'));\n var tops = anchors\n .map(function(anchor) {\n try {\n var rect = anchor.getBoundingClientRect();\n return rect && rect.width > 30 && rect.height > 30 ? rect.top : null;\n } catch(e) {\n return null;\n }\n })\n .filter(function(value) { return typeof value === 'number' && value > 16 && value < window.innerHeight; })\n .sort(function(a, b) { return a - b; });\n return tops.length ? tops[0] : 0;\n }\n\n function getScrollableElements() {\n var elements = [];\n try {\n var all = Array.prototype.slice.call(document.querySelectorAll('body *'));\n for (var i = 0; i < all.length; i++) {\n var el = all[i];\n if (!el || !el.scrollHeight || !el.clientHeight) continue;\n if (el.scrollHeight <= el.clientHeight + 24) continue;\n var style = window.getComputedStyle ? window.getComputedStyle(el) : null;\n var overflowY = style ? (style.overflowY || '') : '';\n var scrollable = overflowY === 'auto' || overflowY === 'scroll' || /webkit-overflow-scrolling/i.test(String(style && style.cssText));\n if (scrollable || el.scrollTop > 0) {\n elements.push(el);\n }\n }\n } catch(e) {}\n\n elements.sort(function(a, b) {\n return (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight);\n });\n return elements.slice(0, 12);\n }\n\n async function scrollAllTargetsTo(position) {\n var targets = getScrollableElements();\n try {\n window.scrollTo(0, position);\n } catch(e) {}\n for (var i = 0; i < targets.length; i++) {\n try {\n targets[i].scrollTop = position;\n } catch(e) {}\n }\n await delay(180);\n }\n\n async function resetProfileScrollToTabs() {\n for (var i = 0; i < 3; i++) {\n try {\n window.scrollTo({ top: 0, left: 0, behavior: 'auto' });\n } catch(e) {\n try { window.scrollTo(0, 0); } catch(_e) {}\n }\n await scrollAllTargetsTo(0);\n }\n try {\n var profileHeader = document.querySelector('[data-e2e=\"user-title\"], h2[data-e2e=\"user-title\"], h2');\n if (profileHeader && profileHeader.scrollIntoView) {\n profileHeader.scrollIntoView({ block: 'start', inline: 'nearest' });\n }\n } catch(e) {}\n await delay(260);\n }\n\n function findExactLikedTabButton() {\n var selectors = [\n 'p[role=\"tab\"][data-e2e=\"liked-tab\"]',\n '[role=\"tab\"][data-e2e=\"liked-tab\"]',\n '[role=\"tab\"][data-e2e*=\"liked\"]',\n 'p[data-e2e=\"liked\"]',\n '[data-e2e=\"liked\"]',\n '[data-e2e=\"liked-tab\"][tabindex]',\n '[data-e2e=\"liked-tab\"]'\n ];\n for (var i = 0; i < selectors.length; i++) {\n try {\n var node = document.querySelector(selectors[i]);\n if (node) return node;\n } catch(e) {}\n }\n return null;\n }\n\n function findTextLikedTabButton() {\n try {\n var tabs = Array.prototype.slice.call(document.querySelectorAll('p, [role=\"tab\"], div, span, button, a'));\n var likedTabs = tabs.filter(function(tab) {\n if (!tab) return false;\n var label = [\n tab.getAttribute('data-e2e') || '',\n tab.getAttribute('aria-label') || '',\n tab.textContent || ''\n ].join(' ').trim().toLowerCase();\n var rect = tab.getBoundingClientRect ? tab.getBoundingClientRect() : null;\n if (!rect || rect.width < 40 || rect.height < 18) return false;\n if (rect.top < 40 || rect.top > (window.innerHeight * 0.6)) return false;\n return (\n label === 'liked' ||\n label.indexOf(' liked') !== -1 ||\n label.indexOf('liked ') !== -1 ||\n label.indexOf('liked') === 0\n );\n });\n if (likedTabs.length === 0) return null;\n likedTabs.sort(function(a, b) {\n var ar = a.getBoundingClientRect ? a.getBoundingClientRect() : { left: 0, top: 0 };\n var br = b.getBoundingClientRect ? b.getBoundingClientRect() : { left: 0, top: 0 };\n return (br.left - ar.left) || (ar.top - br.top);\n });\n return likedTabs[0];\n } catch(e) {\n return null;\n }\n }\n\n function getTabSelectedState(tab) {\n if (!tab) return '';\n try {\n return String(tab.getAttribute('aria-selected') || tab.getAttribute('data-selected') || '').toLowerCase();\n } catch(e) {\n return '';\n }\n }\n\n function findClickableTarget(element) {\n var current = element;\n var depth = 0;\n while (current && depth < 6) {\n if (isClickableElement(current)) return current;\n current = current.parentElement;\n depth++;\n }\n return element;\n }\n\n function hasTabLikeAttributes(el) {\n if (!el || !el.getAttribute) return false;\n var role = (el.getAttribute('role') || '').toLowerCase();\n var data = (el.getAttribute('data-e2e') || '').toLowerCase();\n var tabIndex = el.getAttribute('tabindex');\n if (role === 'tab') return true;\n if (data.indexOf('tab') !== -1) return true;\n if (data.indexOf('liked-tab') !== -1) return true;\n if (data === 'liked') return true;\n if (tabIndex != null && String(tabIndex) !== '') return true;\n return false;\n }\n\n function isWeakLikedLeaf(el) {\n if (!el || !el.getAttribute) return false;\n var tag = el.tagName ? el.tagName.toLowerCase() : '';\n var role = (el.getAttribute('role') || '').toLowerCase();\n var data = (el.getAttribute('data-e2e') || '').toLowerCase();\n var tabIndex = el.getAttribute('tabindex');\n return (\n data === 'liked' &&\n role !== 'tab' &&\n (tabIndex == null || String(tabIndex) === '') &&\n (tag === 'p' || tag === 'span' || tag === 'div')\n );\n }\n\n function resolveLikedInteractionTarget(element) {\n if (!element) return null;\n var current = element;\n var depth = 0;\n var best = element;\n var foundStrongTarget = false;\n while (current && depth < 10) {\n if (current === document.body || current === document.documentElement) {\n break;\n }\n if (hasTabLikeAttributes(current) || isClickableElement(current)) {\n best = current;\n if (hasTabLikeAttributes(current)) {\n if (!isWeakLikedLeaf(current)) {\n foundStrongTarget = true;\n return current;\n }\n }\n }\n current = current.parentElement;\n depth++;\n }\n if (!foundStrongTarget || best === document.body || best === document.documentElement) {\n return element;\n }\n return best;\n }\n\n async function clickLikedTabWithState(tab) {\n if (!tab) {\n return { clicked: false, beforeSelected: '', afterSelected: '', signature: '' };\n }\n var target = resolveLikedInteractionTarget(tab) || findClickableTarget(tab);\n var clickedAny = false;\n var finalBefore = '';\n var finalAfter = '';\n var finalTarget = target;\n var targetRect = target && target.getBoundingClientRect ? target.getBoundingClientRect() : null;\n var centerTapResult = false;\n if (targetRect) {\n centerTapResult = dispatchTapAt(\n Math.round(targetRect.left + (targetRect.width / 2)),\n Math.round(targetRect.top + (targetRect.height / 2))\n );\n }\n var directBefore = getTabSelectedState(target);\n var directClicked = triggerUserClick(target) || centerTapResult;\n await delay(280);\n var directAfter = getTabSelectedState(target);\n if (directAfter === 'true' || directClicked) {\n return {\n clicked: !!directClicked,\n beforeSelected: directBefore,\n afterSelected: directAfter,\n signature: getElementSignature(tab),\n targetSignature: getElementSignature(target),\n };\n }\n\n var attempts = [];\n var seen = [];\n var cursor = target;\n var depth = 0;\n while (cursor && depth < 5) {\n if (cursor === document.body || cursor === document.documentElement) break;\n var role = (cursor.getAttribute && cursor.getAttribute('role') || '').toLowerCase();\n var tag = cursor.tagName ? cursor.tagName.toLowerCase() : '';\n var tabIndex = cursor.getAttribute && cursor.getAttribute('tabindex');\n var isGoodAncestor = role === 'tab' || role === 'button' || tag === 'button' || tag === 'a' || (tabIndex != null && String(tabIndex) !== '');\n if (!isGoodAncestor) {\n cursor = cursor.parentElement;\n depth++;\n continue;\n }\n var sig = getElementSignature(cursor);\n if (sig && seen.indexOf(sig) === -1) {\n seen.push(sig);\n attempts.push(cursor);\n }\n cursor = cursor.parentElement;\n depth++;\n }\n\n for (var i = 0; i < attempts.length; i++) {\n var candidate = attempts[i];\n var before = getTabSelectedState(candidate);\n var rect = candidate.getBoundingClientRect ? candidate.getBoundingClientRect() : null;\n var clickByPoint = rect ? dispatchTapAt(Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2)) : false;\n var clicked = triggerUserClick(candidate) || clickByPoint;\n await delay(260);\n var after = getTabSelectedState(candidate);\n if (clicked) {\n clickedAny = true;\n finalBefore = before;\n finalAfter = after;\n finalTarget = candidate;\n }\n if (after === 'true') {\n return {\n clicked: !!clicked,\n beforeSelected: before,\n afterSelected: after,\n signature: getElementSignature(tab),\n targetSignature: getElementSignature(candidate),\n };\n }\n }\n\n return {\n clicked: !!clickedAny,\n beforeSelected: finalBefore,\n afterSelected: finalAfter,\n signature: getElementSignature(tab),\n targetSignature: getElementSignature(finalTarget),\n };\n }\n\n function getElementMeta(el) {\n if (!el) return null;\n try {\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n return {\n signature: getElementSignature(el),\n tag: el.tagName ? el.tagName.toLowerCase() : '',\n role: el.getAttribute ? (el.getAttribute('role') || '') : '',\n dataE2E: el.getAttribute ? (el.getAttribute('data-e2e') || '') : '',\n ariaSelected: el.getAttribute ? (el.getAttribute('aria-selected') || '') : '',\n className: typeof el.className === 'string' ? el.className.slice(0, 120) : '',\n text: ((el.textContent || '').trim().replace(/s+/g, ' ')).slice(0, 80),\n rect: rect ? toRectString(rect) : '',\n };\n } catch(e) {\n return { signature: getElementSignature(el) };\n }\n }\n\n function getVisibleVideoIds(scopeUsername) {\n return extractVideosFromDom('baseline-visible', scopeUsername).map(function(video) {\n return video && video.videoId ? video.videoId : '';\n }).filter(Boolean);\n }\n\n function collectTopTabLikeElements() {\n try {\n var nodes = Array.prototype.slice.call(document.querySelectorAll('p, [role=\"tab\"], div, span, button, a'))\n .filter(function(el) {\n if (!el || !isVisibleElement(el)) return false;\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!rect) return false;\n if (rect.top < 48 || rect.top > (window.innerHeight * 0.52)) return false;\n if (rect.width < 46 || rect.width > 260) return false;\n if (rect.height < 18 || rect.height > 80) return false;\n var txt = (el.textContent || '').trim().toLowerCase();\n var data = (el.getAttribute && el.getAttribute('data-e2e') || '').toLowerCase();\n if (txt.indexOf('liked') !== -1) return true;\n if (data.indexOf('liked') !== -1 || data.indexOf('tab') !== -1) return true;\n if (el.getAttribute && el.getAttribute('role') === 'tab') return true;\n return false;\n });\n nodes.sort(function(a, b) {\n var ar = a.getBoundingClientRect ? a.getBoundingClientRect() : { left: 0, top: 0 };\n var br = b.getBoundingClientRect ? b.getBoundingClientRect() : { left: 0, top: 0 };\n return (ar.top - br.top) || (br.left - ar.left);\n });\n var unique = [];\n var seen = [];\n for (var i = 0; i < nodes.length; i++) {\n var sig = getElementSignature(nodes[i]);\n if (!sig || seen.indexOf(sig) !== -1) continue;\n seen.push(sig);\n unique.push(nodes[i]);\n if (unique.length >= 8) break;\n }\n return unique;\n } catch(e) {\n return [];\n }\n }\n\n function collectAnonymousTopTabCells() {\n try {\n var elements = Array.prototype.slice.call(document.querySelectorAll('div, p, span, button, a'))\n .filter(function(el) {\n if (!el || !isVisibleElement(el)) return false;\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!rect) return false;\n if (rect.top < 120 || rect.top > (window.innerHeight * 0.45)) return false;\n if (rect.width < 58 || rect.width > 96) return false;\n if (rect.height < 34 || rect.height > 68) return false;\n var txt = (el.textContent || '').trim().toLowerCase();\n if (txt.length > 20) return false;\n return true;\n });\n\n var byRow = {};\n for (var i = 0; i < elements.length; i++) {\n var rect = elements[i].getBoundingClientRect();\n var rowKey = String(Math.round((rect.top + rect.height / 2) / 6) * 6);\n if (!byRow[rowKey]) byRow[rowKey] = [];\n byRow[rowKey].push(elements[i]);\n }\n\n var bestRow = null;\n var bestScore = -1;\n var keys = Object.keys(byRow);\n for (var k = 0; k < keys.length; k++) {\n var row = byRow[keys[k]].sort(function(a, b) {\n return a.getBoundingClientRect().left - b.getBoundingClientRect().left;\n });\n if (row.length < 4) continue;\n var first = row[0].getBoundingClientRect();\n var last = row[row.length - 1].getBoundingClientRect();\n var spread = last.right - first.left;\n if (spread < window.innerWidth * 0.58) continue;\n var y = parseInt(keys[k], 10);\n var score = (row.length * 120) + Math.min(260, spread) - Math.abs(218 - y);\n if (score > bestScore) {\n bestScore = score;\n bestRow = row;\n }\n }\n\n if (!bestRow) return [];\n return bestRow.slice().reverse(); // right-most first\n } catch(e) {\n return [];\n }\n }\n\n async function tryOpenLikedByTabCandidates(username) {\n await resetProfileScrollToTabs();\n var baselineIds = getVisibleVideoIds(username).join('|');\n var candidates = [];\n var appendCandidate = function(node) {\n if (!node) return;\n if (candidates.indexOf(node) === -1) candidates.push(node);\n };\n\n appendCandidate(findExactLikedTabButton());\n var textLikedCandidate = findTextLikedTabButton();\n appendCandidate(textLikedCandidate);\n appendCandidate(resolveLikedInteractionTarget(textLikedCandidate));\n appendCandidate(findLikedTabButton());\n var collected = collectTopTabLikeElements();\n for (var i = 0; i < collected.length; i++) appendCandidate(collected[i]);\n var anonymousCells = collectAnonymousTopTabCells();\n for (var j = 0; j < anonymousCells.length; j++) appendCandidate(anonymousCells[j]);\n\n var candidateLogs = [];\n if (candidates.length === 0) {\n return {\n opened: false,\n videos: [],\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n mode: 'candidate-tab-failed',\n reason: 'NO_CANDIDATES',\n tabCandidates: collectLikedTabCandidates(),\n candidates: candidateLogs\n }\n };\n }\n for (var ci = 0; ci < candidates.length; ci++) {\n var candidate = candidates[ci];\n var beforeMeta = getElementMeta(candidate);\n var clickState = await clickLikedTabWithState(candidate);\n await delay(950);\n var likedContainer = findLikedVideoContainer();\n if (likedContainer) {\n var containerVideos = extractVideosFromDom('liked-dom-candidate-container', username, likedContainer);\n if (containerVideos.length > 0 && !linksLookLikeOwnPosts(containerVideos, username)) {\n return {\n opened: true,\n videos: containerVideos,\n containerFound: true,\n coordinateTapped: true,\n coordinate: {\n mode: 'candidate-tab-container',\n clicked: beforeMeta,\n clickState: clickState,\n candidates: candidateLogs\n }\n };\n }\n }\n var videos = extractVideosFromDom('liked-dom-candidate', username);\n var afterIds = videos.map(function(video) { return video.videoId; }).join('|');\n candidateLogs.push({\n clicked: beforeMeta,\n clickState: clickState,\n videoCount: videos.length,\n changedFromBaseline: afterIds !== baselineIds,\n });\n if (videos.length > 0 && afterIds !== baselineIds && !linksLookLikeOwnPosts(videos, username)) {\n return {\n opened: true,\n videos: videos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n mode: 'candidate-tab',\n clicked: beforeMeta,\n clickState: clickState,\n candidates: candidateLogs\n }\n };\n }\n }\n\n return {\n opened: false,\n videos: [],\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n mode: 'candidate-tab-failed',\n tabCandidates: collectLikedTabCandidates(),\n candidates: candidateLogs\n }\n };\n }\n\n function getPrivateAccountNotice() {\n try {\n var candidates = Array.prototype.slice.call(document.querySelectorAll('div, p, span, h2, h3'));\n for (var i = 0; i < candidates.length; i++) {\n if (!isVisibleElement(candidates[i])) continue;\n var rect = candidates[i].getBoundingClientRect ? candidates[i].getBoundingClientRect() : null;\n if (!rect || rect.top > window.innerHeight * 0.92 || rect.bottom < 24) continue;\n var value = (candidates[i].textContent || '').trim().toLowerCase();\n if (!value || value.length < 18 || value.length > 420) continue;\n var match = value.match(/this account is private[^.]{0,140}|follow this account[^.]{0,140}|this user'?s liked videos are private[^.]{0,140}|liked videos are private[^.]{0,140}/i);\n if (match && match[0]) {\n return String(match[0]).trim().slice(0, 180);\n }\n }\n } catch(e) {}\n return '';\n }\n\n function getManualDebugConfig() {\n try {\n var enabled = !!window.__ONAIROS_TIKTOK_MANUAL_DEBUG;\n var passiveLogOnly = !!window.__ONAIROS_TIKTOK_PASSIVE_LOG_ONLY;\n var timeoutMsRaw = Number(window.__ONAIROS_TIKTOK_MANUAL_TIMEOUT_MS || 0);\n var timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 120000;\n var passiveWarmupRaw = Number(window.__ONAIROS_TIKTOK_PASSIVE_WARMUP_MS || 0);\n var passiveWarmupMs = Number.isFinite(passiveWarmupRaw) && passiveWarmupRaw > 0 ? passiveWarmupRaw : 7000;\n return {\n enabled: enabled,\n passiveLogOnly: passiveLogOnly,\n timeoutMs: Math.max(10000, timeoutMs),\n passiveWarmupMs: Math.max(5000, passiveWarmupMs)\n };\n } catch (e) {\n return { enabled: false, passiveLogOnly: false, timeoutMs: 120000, passiveWarmupMs: 7000 };\n }\n }\n\n function getVideoIdList(videos, max) {\n return (videos || []).map(function(video) { return video && video.videoId; }).filter(Boolean).slice(0, max || 10);\n }\n\n function collectManualSnapshot(username, baselineIds) {\n var state = getLikedTabDebugState();\n var videos = extractVideosFromDom('manual-debug-visible', username);\n var ids = getVideoIdList(videos, 12);\n var privateNotice = getPrivateAccountNotice();\n var liked = findExactLikedTabButton() || findTextLikedTabButton();\n var likedRect = liked && liked.getBoundingClientRect ? liked.getBoundingClientRect() : null;\n var points = [];\n if (likedRect) {\n points = [\n { x: Math.round(likedRect.left + likedRect.width * 0.5), y: Math.round(likedRect.top + likedRect.height * 0.5) },\n { x: Math.round(likedRect.left + likedRect.width * 0.85), y: Math.round(likedRect.top + likedRect.height * 0.5) }\n ];\n } else {\n var y = state.firstGridTop ? Math.max(88, state.firstGridTop - 42) : 120;\n points = [\n { x: Math.round(window.innerWidth * 0.5), y: y },\n { x: Math.round(window.innerWidth * 0.82), y: y },\n ];\n }\n var pointTargets = points.map(function(point) {\n return {\n x: point.x,\n y: point.y,\n target: getElementSignature(document.elementFromPoint(point.x, point.y))\n };\n });\n return {\n path: location.pathname,\n state: state,\n privateNotice: privateNotice,\n visibleVideoCount: videos.length,\n visibleVideoIds: ids,\n changedFromBaseline: ids.join('|') !== (baselineIds || ''),\n pointTargets: pointTargets,\n tabCandidates: collectLikedTabCandidates(),\n };\n }\n\n async function runPassiveManualLogging(timeoutMs) {\n var started = Date.now();\n var tick = 0;\n var overrideUsername = getProfileUsernameOverride();\n if (overrideUsername) {\n var profileUrl = 'https://www.tiktok.com/@' + encodeURIComponent(overrideUsername);\n var routedToProfile = routeToProfileUrl(profileUrl);\n debug('manual:passive-route-profile', {\n overrideUsername: overrideUsername,\n profileUrl: profileUrl,\n routed: routedToProfile,\n path: location.pathname\n });\n await delay(700);\n }\n debug('manual:passive-start', {\n timeoutMs: timeoutMs,\n instructions: 'Passive log mode active. Use normal TikTok feed/profile manually while logs stream.'\n });\n while (Date.now() - started < timeoutMs) {\n tick += 1;\n var snapshot = collectManualSnapshot('', '');\n debug('manual:passive-tick', Object.assign({\n tick: tick,\n elapsedMs: Date.now() - started\n }, snapshot));\n // Exit early once profile content is clearly loaded and we logged enough samples.\n if (tick >= 3 && snapshot && snapshot.visibleVideoCount > 0) {\n debug('manual:passive-ready', {\n tick: tick,\n elapsedMs: Date.now() - started,\n visibleVideoCount: snapshot.visibleVideoCount\n });\n return;\n }\n await delay(1500);\n }\n debug('manual:passive-end', { elapsedMs: Date.now() - started });\n }\n\n async function waitForManualLikedContext(username, timeoutMs) {\n var started = Date.now();\n var baselineVideos = extractVideosFromDom('manual-debug-baseline', username);\n var baselineIds = getVideoIdList(baselineVideos, 20).join('|');\n var tick = 0;\n debug('manual:start', {\n timeoutMs: timeoutMs,\n baselineCount: baselineVideos.length,\n baselineIds: getVideoIdList(baselineVideos, 12),\n instructions: 'Manually open your profile, tap Liked tab, wait on that screen.'\n });\n\n while (Date.now() - started < timeoutMs) {\n tick += 1;\n var snapshot = collectManualSnapshot(username, baselineIds);\n debug('manual:tick', Object.assign({ tick: tick, elapsedMs: Date.now() - started }, snapshot));\n\n var onLikedPath = /\\/@[^/]+\\/liked\\/?$/i.test(location.pathname || '');\n var hasLikedContainer = !!findLikedVideoContainer();\n var hasNonOwnVideos = snapshot.visibleVideoCount > 0 && !linksLookLikeOwnPosts(\n extractVideosFromDom('manual-debug-check', username),\n username\n );\n if (onLikedPath || hasLikedContainer || hasNonOwnVideos) {\n debug('manual:ready', {\n tick: tick,\n elapsedMs: Date.now() - started,\n onLikedPath: onLikedPath,\n hasLikedContainer: hasLikedContainer,\n hasNonOwnVideos: hasNonOwnVideos\n });\n return { completed: true, baselineIds: baselineIds };\n }\n await delay(1500);\n }\n\n debug('manual:timeout', { elapsedMs: Date.now() - started });\n return { completed: false, baselineIds: baselineIds };\n }\n\n function collectLikedTabCandidates() {\n try {\n return Array.prototype.slice.call(document.querySelectorAll('p[role=\"tab\"], [role=\"tab\"], [data-e2e*=\"tab\"], [data-e2e*=\"liked\"]'))\n .map(function(el) {\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n return getElementSignature(el) + (rect ? (' @ ' + toRectString(rect)) : '');\n })\n .filter(Boolean)\n .slice(0, 24);\n } catch(e) {\n return [];\n }\n }\n\n async function waitForLikedTabReady(timeoutMs) {\n var started = Date.now();\n var lastSnapshot = null;\n while (Date.now() - started < timeoutMs) {\n var exact = findExactLikedTabButton();\n var textLiked = findTextLikedTabButton();\n var generic = findLikedTabButton();\n var privateNotice = getPrivateAccountNotice();\n var textPrivate = textLiked && isVisibleElement(textLiked) && /liked videos are private/i.test((textLiked.textContent || ''));\n lastSnapshot = {\n path: location.pathname,\n scrollY: window.scrollY || 0,\n firstGridTop: getFirstVideoGridTop(),\n hasExactLikedTab: !!exact,\n exactLikedSignature: getElementSignature(exact),\n hasTextLikedTab: !!textLiked,\n textLikedSignature: getElementSignature(textLiked),\n hasGenericLiked: !!generic,\n genericLikedSignature: getElementSignature(generic),\n privateNotice: privateNotice || (textPrivate ? 'This user liked videos are private' : ''),\n tabCandidates: collectLikedTabCandidates(),\n };\n if (exact || textLiked || generic) {\n return {\n ready: true,\n exact: exact || null,\n textLiked: textLiked || null,\n generic: generic || null,\n snapshot: lastSnapshot\n };\n }\n await delay(350);\n }\n return { ready: false, exact: null, textLiked: null, generic: null, snapshot: lastSnapshot };\n }\n\n async function waitForProfileContentReady(timeoutMs) {\n var started = Date.now();\n while (Date.now() - started < timeoutMs) {\n var hasHeader = !!document.querySelector('[data-e2e=\"user-title\"], h2[data-e2e=\"user-title\"], h2');\n var firstGridTop = getFirstVideoGridTop();\n if (hasHeader || firstGridTop > 0) {\n return { ready: true, hasHeader: hasHeader, firstGridTop: firstGridTop };\n }\n await delay(250);\n }\n return { ready: false, hasHeader: !!document.querySelector('[data-e2e=\"user-title\"], h2[data-e2e=\"user-title\"], h2'), firstGridTop: getFirstVideoGridTop() };\n }\n\n function getLikedTabDebugState() {\n var likedTab = findExactLikedTabButton();\n var textLikedTab = findTextLikedTabButton();\n var genericLiked = findLikedTabButton();\n var activeTab = null;\n try {\n activeTab = document.querySelector('[role=\"tab\"][aria-selected=\"true\"]');\n } catch(e) {}\n return {\n path: location.pathname,\n scrollY: window.scrollY || 0,\n firstGridTop: getFirstVideoGridTop(),\n hasExactLikedTab: !!likedTab,\n exactLikedSignature: getElementSignature(likedTab),\n exactLikedSelected: !!(likedTab && String(likedTab.getAttribute('aria-selected') || '').toLowerCase() === 'true'),\n hasTextLikedTab: !!textLikedTab,\n textLikedSignature: getElementSignature(textLikedTab),\n textLikedSelected: !!(textLikedTab && String(textLikedTab.getAttribute('aria-selected') || '').toLowerCase() === 'true'),\n hasGenericLiked: !!genericLiked,\n genericLikedSignature: getElementSignature(genericLiked),\n activeTabSignature: getElementSignature(activeTab),\n };\n }\n\n function captureTapProbe(firstGridTop, rowCandidate) {\n var y = rowCandidate && rowCandidate.rowY\n ? rowCandidate.rowY\n : (firstGridTop ? Math.max(72, firstGridTop - 18) : Math.round(window.innerHeight * 0.56));\n var xPoints = [0.18, 0.5, 0.82].map(function(mult) { return Math.round(window.innerWidth * mult); });\n return xPoints.map(function(x) {\n var el = document.elementFromPoint(x, y);\n return { x: x, y: y, target: getElementSignature(el) };\n });\n }\n\n async function tapMobileLikedTabByPosition(username) {\n await resetProfileScrollToTabs();\n var firstGridTop = getFirstVideoGridTop();\n var beforeIds = extractVideosFromDom('before-liked-coordinate', username).map(function(video) { return video.videoId; }).join('|');\n var directLikedTab = findExactLikedTabButton() || findTextLikedTabButton();\n if (directLikedTab) {\n var tabRect = directLikedTab.getBoundingClientRect ? directLikedTab.getBoundingClientRect() : null;\n var tabPoints = tabRect ? [\n { x: Math.round(tabRect.left + tabRect.width * 0.5), y: Math.round(tabRect.top + tabRect.height * 0.5) },\n { x: Math.round(tabRect.left + tabRect.width * 0.7), y: Math.round(tabRect.top + tabRect.height * 0.5) },\n { x: Math.round(tabRect.left + tabRect.width * 0.85), y: Math.round(tabRect.top + tabRect.height * 0.45) },\n { x: Math.round(tabRect.right - 8), y: Math.round(tabRect.top + tabRect.height * 0.5) }\n ] : [];\n for (var dti = 0; dti < tabPoints.length; dti++) {\n dispatchTapAt(tabPoints[dti].x, tabPoints[dti].y);\n triggerUserClick(directLikedTab);\n await delay(900);\n var exactVideos = extractVideosFromDom('liked-dom-exact-tab', username);\n var exactIds = exactVideos.map(function(video) { return video.videoId; }).join('|');\n if (exactVideos.length > 0 && exactIds !== beforeIds && !linksLookLikeOwnPosts(exactVideos, username)) {\n return {\n opened: true,\n videos: exactVideos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n mode: 'exact-liked-tab-rect',\n target: getElementSignature(directLikedTab),\n point: tabPoints[dti]\n }\n };\n }\n }\n }\n var rowCandidate = detectIconTabRowNearGrid(firstGridTop);\n if (isStatsLikeRow(rowCandidate) || isStatsLikeRowBySignature(rowCandidate)) {\n rowCandidate = null;\n }\n if (!rowCandidate) {\n var anonymousRowCandidate = detectAnonymousTabCellRow(firstGridTop);\n if (anonymousRowCandidate && isLikelyTabCellRow(anonymousRowCandidate, firstGridTop)) {\n rowCandidate = anonymousRowCandidate;\n }\n }\n if (!rowCandidate) {\n var profileRowCandidate = detectProfileTabRow(firstGridTop);\n if (\n profileRowCandidate &&\n (\n (!isStatsLikeRow(profileRowCandidate) && !isStatsLikeRowBySignature(profileRowCandidate) && isLikelyTabCellRow(profileRowCandidate, firstGridTop)) ||\n (profileRowCandidate.elements && profileRowCandidate.elements.length >= 4)\n )\n ) {\n rowCandidate = profileRowCandidate;\n }\n }\n var baseTabY = rowCandidate && rowCandidate.rowY\n ? rowCandidate.rowY\n : (firstGridTop ? Math.max(96, firstGridTop - 44) : Math.round(window.innerHeight * 0.30));\n var attemptLog = [];\n if (rowCandidate && rowCandidate.elements && rowCandidate.elements.length >= 3) {\n var rowTargets = rowCandidate.elements.map(function(entry) { return entry.el; });\n var preferenceOrder = rowTargets.slice().reverse().filter(Boolean);\n\n for (var t = 0; t < preferenceOrder.length; t++) {\n var targetEl = preferenceOrder[t];\n try {\n var rect = targetEl && targetEl.getBoundingClientRect ? targetEl.getBoundingClientRect() : null;\n if (rect) {\n dispatchTapAt(Math.round(rect.left + rect.width / 2), Math.round(rect.top + rect.height / 2));\n attemptLog.push({\n mode: 'row-target-dispatch',\n target: getElementSignature(targetEl),\n rect: toRectString(rect)\n });\n await delay(220);\n }\n } catch(e) {}\n triggerUserClick(targetEl);\n attemptLog.push({\n mode: 'row-target-click',\n target: getElementSignature(targetEl)\n });\n await delay(1150);\n var directVideos = extractVideosFromDom('liked-dom-row-target', username);\n var directIds = directVideos.map(function(video) { return video.videoId; }).join('|');\n if (directVideos.length > 0 && directIds !== beforeIds && !linksLookLikeOwnPosts(directVideos, username)) {\n return {\n opened: true,\n videos: directVideos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n mode: 'row-target',\n rowY: rowCandidate.rowY,\n target: getElementSignature(preferenceOrder[t]),\n attempts: attemptLog.slice(0, 10)\n }\n };\n }\n }\n }\n\n var xPositions = [\n Math.round(window.innerWidth * 0.82),\n Math.round(window.innerWidth * 0.75),\n Math.round(window.innerWidth * 0.68),\n Math.round(window.innerWidth * 0.67),\n Math.round(window.innerWidth * 0.58),\n Math.round(window.innerWidth * 0.50)\n ];\n var yOffsets = [-36, -24, -12, 0, 12, 24, 36];\n\n for (var yi = 0; yi < yOffsets.length; yi++) {\n var tabY = Math.max(84, baseTabY + yOffsets[yi]);\n for (var i = 0; i < xPositions.length; i++) {\n dispatchTapAt(xPositions[i], tabY);\n attemptLog.push({\n mode: 'coordinate-dispatch',\n x: xPositions[i],\n y: tabY,\n pointTarget: getElementSignature(document.elementFromPoint(xPositions[i], tabY))\n });\n await delay(1150);\n\n var immediateVideos = extractVideosFromDom('liked-dom-coordinate-immediate', username);\n var immediateIds = immediateVideos.map(function(video) { return video.videoId; }).join('|');\n if (immediateVideos.length > 0 && immediateIds !== beforeIds && !linksLookLikeOwnPosts(immediateVideos, username)) {\n return {\n opened: true,\n videos: immediateVideos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { x: xPositions[i], y: tabY, attempts: attemptLog.slice(0, 14) }\n };\n }\n\n await scrollLikedGrid();\n var videos = extractVideosFromDom('liked-dom-coordinate', username);\n var afterIds = videos.map(function(video) { return video.videoId; }).join('|');\n if (videos.length > 0 && afterIds !== beforeIds && !linksLookLikeOwnPosts(videos, username)) {\n return {\n opened: true,\n videos: videos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { x: xPositions[i], y: tabY, attempts: attemptLog.slice(0, 14) }\n };\n }\n }\n }\n\n return {\n opened: false,\n videos: [],\n containerFound: false,\n coordinateTapped: true,\n coordinate: {\n y: baseTabY,\n triedX: xPositions,\n triedYOffset: yOffsets,\n firstGridTop: firstGridTop,\n attempts: attemptLog.slice(0, 18),\n rowCandidate: rowCandidate ? {\n rowY: rowCandidate.rowY,\n elements: rowCandidate.elements.slice(0, 5).map(function(entry) {\n return getElementSignature(entry.el) + ' @ ' + toRectString(entry.rect);\n })\n } : null\n },\n diagnostics: collectTabDiagnostics({ firstGridTop: firstGridTop, rowCandidate: rowCandidate })\n };\n }\n\n function collectTabDiagnostics(context) {\n var firstGridTop = context && typeof context.firstGridTop === 'number' ? context.firstGridTop : getFirstVideoGridTop();\n try {\n var labels = Array.prototype.slice.call(document.querySelectorAll('span, [role=\"tab\"], button, [tabindex], div'))\n .map(function(el) {\n var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n var pos = rect ? Math.round(rect.left) + ',' + Math.round(rect.top) + ',' + Math.round(rect.width) + 'x' + Math.round(rect.height) : '';\n var combined = [getElementSignature(el), pos].filter(Boolean).join(' @ ');\n return combined.length > 160 ? combined.slice(0, 160) : combined;\n })\n .filter(function(value) { return value && /liked|likes|favorite|video|post|lock|tab/i.test(value); })\n .slice(0, 30);\n var rowCandidate = (context && context.rowCandidate) || detectProfileTabRow(firstGridTop);\n var probes = captureTapProbe(firstGridTop, rowCandidate);\n return {\n path: location.pathname,\n viewport: { width: window.innerWidth, height: window.innerHeight },\n scroll: {\n windowY: window.scrollY || 0,\n bodyTop: document.body && document.body.getBoundingClientRect ? Math.round(document.body.getBoundingClientRect().top) : 0\n },\n firstGridTop: firstGridTop,\n probeTargets: probes,\n rowCandidate: rowCandidate ? {\n rowY: rowCandidate.rowY,\n elements: rowCandidate.elements.slice(0, 6).map(function(entry) {\n return getElementSignature(entry.el) + ' @ ' + toRectString(entry.rect);\n })\n } : null,\n labels: labels\n };\n } catch(e) {\n return { error: String(e && e.message ? e.message : e) };\n }\n }\n\n async function scrollLikedGrid() {\n for (var i = 0; i < 4; i++) {\n try {\n window.scrollTo(0, document.body.scrollHeight);\n } catch(e) {}\n var targets = getScrollableElements();\n for (var j = 0; j < targets.length; j++) {\n try {\n targets[j].scrollTop = Math.max(targets[j].scrollTop, targets[j].scrollHeight);\n } catch(e) {}\n }\n await delay(700);\n }\n }\n\n function isOwnProfilePath(pathname) {\n return /^\\/@[^/]+\\/?$/i.test(pathname || '');\n }\n\n function absolutizeTikTokUrl(href) {\n if (!href || typeof href !== 'string') return '';\n try {\n return new URL(href, 'https://www.tiktok.com').href;\n } catch(e) {\n return '';\n }\n }\n\n function isProfileUrl(href) {\n if (!href) return false;\n try {\n var parsed = new URL(href, 'https://www.tiktok.com');\n return /\\/(@[^/]+)\\/?$/i.test(parsed.pathname || '');\n } catch(e) {\n return false;\n }\n }\n\n function getProfileUsernameOverride() {\n try {\n var value = window.__ONAIROS_TIKTOK_PROFILE_USERNAME;\n if (!value || typeof value !== 'string') return '';\n return value.replace(/^@/, '').trim();\n } catch(e) {\n return '';\n }\n }\n\n function getCurrentProfileUsername() {\n var currentProfileMatch = location.pathname.match(/^\\/@([^/]+)\\/?$/);\n return currentProfileMatch ? decodeURIComponent(currentProfileMatch[1]).replace(/^@/, '') : '';\n }\n\n /**\n * Detects the logged-in user's TikTok handle by parsing the current page's\n * hydration scripts (__UNIVERSAL_DATA_FOR_REHYDRATION__ / SIGI_STATE).\n *\n * TikTok's webapp embeds the current session user under known keys; we\n * check them in order of specificity, falling back to a generic deep scan\n * for any object that carries both a uniqueId AND looks like the logged-in\n * user (has private/login/app-context signals rather than being some other\n * user's UserModule entry).\n *\n * Returns '' when no logged-in user can be confidently identified.\n */\n function detectLoggedInUsernameFromHydration() {\n try {\n var states = parseHydrationStates(document);\n var sanitize = function(value) {\n if (typeof value !== 'string') return '';\n return value.replace(/^@/, '').trim();\n };\n\n for (var s = 0; s < states.length; s++) {\n var state = states[s];\n if (!state || typeof state !== 'object') continue;\n\n // __UNIVERSAL_DATA_FOR_REHYDRATION__ shape (current TikTok webapp).\n var defaultScope = state.__DEFAULT_SCOPE__ || state.default_scope || state.defaultScope;\n if (defaultScope && typeof defaultScope === 'object') {\n var appContext = defaultScope['webapp.app-context'] || defaultScope['webapp.user-detail'];\n var ctxUser = appContext && appContext.user;\n if (ctxUser) {\n var ctxName = sanitize(ctxUser.uniqueId || ctxUser.unique_id || ctxUser.nickName || ctxUser.nickname);\n if (ctxName) return ctxName;\n }\n }\n\n // Legacy SIGI_STATE shape \u2014 AppContext / UserModule / userInfo.\n var legacyCtx = state.AppContext || state.appContext;\n var legacyUser = legacyCtx && (legacyCtx.user || (legacyCtx.userInfo && legacyCtx.userInfo.user));\n if (legacyUser) {\n var legacyName = sanitize(legacyUser.uniqueId || legacyUser.unique_id || legacyUser.nickname);\n if (legacyName) return legacyName;\n }\n\n // Heuristic deep scan: an object carrying a uniqueId together with\n // signals that mark it as the *current* session user (rather than\n // some random profile from a video feed). We look for app-context\n // markers like aid/appId/isLogin, or \"self\" markers on the node.\n var candidates = findByKeys(state, function(node) {\n if (!node || typeof node !== 'object') return false;\n if (!(node.uniqueId || node.unique_id)) return false;\n return !!(\n node.isLogin === true ||\n node.is_login === true ||\n node.isSelf === true ||\n node.is_self === true ||\n node.appId ||\n node.app_id ||\n node.aid ||\n (node.user && (node.user.uniqueId || node.user.unique_id))\n );\n }, 5);\n\n for (var i = 0; i < candidates.length; i++) {\n var c = candidates[i];\n var nested = c && c.user && (c.user.uniqueId || c.user.unique_id);\n var name = sanitize(nested || c.uniqueId || c.unique_id);\n if (name) return name;\n }\n }\n } catch (e) {}\n return '';\n }\n\n function routeToProfileUrl(profileUrl) {\n try {\n var parsed = new URL(profileUrl, 'https://www.tiktok.com');\n var nextPath = parsed.pathname || '';\n if (!/^\\/@[^/]+\\/?$/i.test(nextPath)) return false;\n history.pushState(null, '', nextPath);\n try {\n window.dispatchEvent(typeof PopStateEvent !== 'undefined' ? new PopStateEvent('popstate') : new Event('popstate'));\n } catch(e) {}\n return true;\n } catch(e) {\n return false;\n }\n }\n\n async function tryExtractFromLikedRoute(username) {\n if (!username) return null;\n var likedUrl = 'https://www.tiktok.com/@' + encodeURIComponent(String(username).replace(/^@/, '')) + '/liked';\n debug('liked:route-liked-attempt', { username: username, fetchUrl: likedUrl, path: location.pathname });\n try {\n var response = await fetch(likedUrl, {\n method: 'GET',\n credentials: 'include',\n headers: { 'Accept': 'text/html,application/xhtml+xml' },\n });\n if (!response.ok) {\n debug('liked:route-liked-empty', { reason: 'FETCH_NOT_OK', status: response.status, path: location.pathname });\n return null;\n }\n var html = await response.text();\n var doc = new DOMParser().parseFromString(html, 'text/html');\n var states = parseHydrationStates(doc);\n var extracted = extractFromStates(states, username, 'liked-fetch-hydration');\n var videos = (extracted && extracted.videos ? extracted.videos : []).filter(function(video) {\n return !!(video && video.videoId);\n });\n if (videos.length > 0 && !linksLookLikeOwnPosts(videos, username)) {\n return {\n opened: true,\n videos: videos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { mode: 'route-liked-fetch', path: location.pathname, count: videos.length }\n };\n }\n debug('liked:route-liked-empty', {\n reason: 'NO_LIKED_VIDEOS_FROM_FETCH',\n path: location.pathname,\n count: videos.length,\n });\n return null;\n } catch (e) {\n debug('liked:route-liked-empty', {\n reason: 'FETCH_EXCEPTION',\n path: location.pathname,\n message: String(e && e.message ? e.message : e),\n });\n return null;\n }\n }\n\n function resolveSecUidFromStates(states, username) {\n var target = (username || '').toLowerCase();\n var secUid = '';\n (states || []).forEach(function(state) {\n if (secUid) return;\n var users = findByKeys(state, function(node) {\n if (!node || typeof node !== 'object') return false;\n var uniqueId = String(node.uniqueId || node.username || '').toLowerCase();\n var nodeSecUid = String(node.secUid || node.sec_uid || '');\n if (!nodeSecUid) return false;\n if (!target) return true;\n return uniqueId === target;\n }, 50);\n for (var i = 0; i < users.length; i++) {\n var candidate = users[i];\n var value = candidate && (candidate.secUid || candidate.sec_uid);\n if (value) {\n secUid = String(value);\n break;\n }\n }\n });\n return secUid;\n }\n\n function extractVideoArrayFromApiPayload(payload) {\n if (!payload || typeof payload !== 'object') return [];\n var buckets = [\n payload.itemList,\n payload.item_list,\n payload.items,\n payload.aweme_list,\n payload.videoList,\n payload.data && payload.data.itemList,\n payload.data && payload.data.item_list,\n payload.data && payload.data.items,\n payload.body && payload.body.itemList,\n payload.body && payload.body.item_list\n ];\n for (var i = 0; i < buckets.length; i++) {\n if (Array.isArray(buckets[i]) && buckets[i].length > 0) return buckets[i];\n }\n return [];\n }\n\n async function fetchLikedVideosViaApi(username, states) {\n if (!username) return null;\n var secUid = resolveSecUidFromStates(states, username);\n var base = 'https://www.tiktok.com';\n var endpoints = [];\n if (secUid) {\n endpoints.push(base + '/api/favorite/item_list/?aid=1988&count=35&cursor=0&secUid=' + encodeURIComponent(secUid));\n endpoints.push(base + '/api/user/liked/list/?aid=1988&count=35&cursor=0&secUid=' + encodeURIComponent(secUid));\n endpoints.push(base + '/api/item_list/?aid=1988&count=35&cursor=0&type=2&secUid=' + encodeURIComponent(secUid));\n }\n endpoints.push(base + '/api/user/liked/list/?aid=1988&count=35&cursor=0&uniqueId=' + encodeURIComponent(username));\n\n var attemptLog = [];\n var seen = {};\n var videos = [];\n for (var i = 0; i < endpoints.length; i++) {\n var url = endpoints[i];\n try {\n var response = await fetch(url, {\n method: 'GET',\n credentials: 'include',\n headers: { 'Accept': 'application/json,text/plain,*/*' },\n });\n var status = response.status;\n var body = null;\n try {\n body = await response.json();\n } catch (e) {\n attemptLog.push({ endpoint: url, status: status, parse: 'json-failed' });\n continue;\n }\n\n var rawItems = extractVideoArrayFromApiPayload(body);\n var normalized = rawItems.map(function(item) {\n return normalizeVideo(item, username, 'liked-api');\n }).filter(function(video) { return !!(video && video.videoId); });\n\n for (var vi = 0; vi < normalized.length; vi++) {\n var key = normalized[vi].videoId;\n if (!seen[key]) {\n seen[key] = true;\n videos.push(normalized[vi]);\n }\n }\n\n attemptLog.push({\n endpoint: url,\n status: status,\n rawCount: rawItems.length,\n normalizedCount: normalized.length\n });\n\n if (videos.length > 0 && !linksLookLikeOwnPosts(videos, username)) {\n debug('liked:api-liked-success', {\n endpoint: url,\n secUidFound: !!secUid,\n attempts: attemptLog\n });\n return {\n opened: true,\n videos: videos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { mode: 'liked-api', endpoint: url, attempts: attemptLog }\n };\n }\n } catch (e) {\n attemptLog.push({ endpoint: url, status: 'fetch-error', message: String(e && e.message ? e.message : e) });\n }\n }\n\n debug('liked:api-liked-empty', {\n secUidFound: !!secUid,\n attempts: attemptLog,\n totalVideos: videos.length\n });\n return null;\n }\n\n async function waitForProfileRoute(expectedUsername, timeoutMs) {\n var startedAt = Date.now();\n var expected = expectedUsername ? expectedUsername.toLowerCase() : '';\n while (Date.now() - startedAt < timeoutMs) {\n var currentUsername = getCurrentProfileUsername();\n var domProfile = extractProfileFromDom();\n var domUsername = domProfile && domProfile.username ? domProfile.username.toLowerCase() : '';\n var pathMatches = currentUsername && (!expected || currentUsername.toLowerCase() === expected);\n var domMatches = domUsername && (!expected || domUsername === expected);\n if (pathMatches && (domMatches || document.querySelector('a[href*=\"/video/\"], [data-e2e=\"user-bio\"], [data-e2e=\"user-title\"]'))) {\n return true;\n }\n await delay(350);\n }\n return false;\n }\n\n function findOwnProfileUrl() {\n var overrideUsername = getProfileUsernameOverride();\n\n var explicitSelectors = [\n 'a[data-e2e=\"nav-profile\"]',\n '[data-e2e=\"nav-profile\"] a[href*=\"/@\"]',\n '[data-e2e=\"profile-icon\"] a[href*=\"/@\"]',\n '[data-e2e=\"user-avatar\"] a[href*=\"/@\"]',\n '[data-e2e=\"profile-button\"] a[href*=\"/@\"]',\n '[data-e2e=\"profile-link\"]',\n '[data-e2e^=\"nav-\"] a[href*=\"/@\"]',\n 'a[href*=\"/@\"][aria-label*=\"profile\" i]',\n 'a[href*=\"/@\"][title*=\"profile\" i]'\n ];\n\n for (var si = 0; si < explicitSelectors.length; si++) {\n try {\n var el = document.querySelector(explicitSelectors[si]);\n var href = el && (el.href || el.getAttribute('href'));\n var absolute = absolutizeTikTokUrl(href);\n if (isProfileUrl(absolute)) return absolute;\n } catch(e) {}\n }\n\n var anchors = Array.prototype.slice.call(document.querySelectorAll('a[href*=\"/@\"]:not([href*=\"/video/\"])'));\n for (var i = 0; i < anchors.length; i++) {\n var anchor = anchors[i];\n var label = [\n anchor.getAttribute('aria-label') || '',\n anchor.getAttribute('title') || '',\n anchor.getAttribute('data-e2e') || '',\n anchor.textContent || ''\n ].join(' ').toLowerCase();\n var inNav = !!anchor.closest('nav, aside, header, [role=\"navigation\"]');\n var looksLikeSelfProfile = inNav && (\n label.indexOf('profile') !== -1 ||\n label.indexOf('view profile') !== -1 ||\n label.trim() === 'me'\n );\n if (!looksLikeSelfProfile) continue;\n var absoluteHref = absolutizeTikTokUrl(anchor.href || anchor.getAttribute('href'));\n if (isProfileUrl(absoluteHref)) return absoluteHref;\n }\n\n // Hydration fallback: parse __UNIVERSAL_DATA_FOR_REHYDRATION__ / SIGI_STATE\n // for the logged-in user's uniqueId. Way more reliable than DOM selectors\n // because TikTok ships the session user in the page bootstrap regardless\n // of which sidebar variant rendered.\n var hydrationUsername = detectLoggedInUsernameFromHydration();\n if (hydrationUsername) {\n return 'https://www.tiktok.com/@' + encodeURIComponent(hydrationUsername);\n }\n\n if (overrideUsername) {\n return 'https://www.tiktok.com/@' + encodeURIComponent(overrideUsername);\n }\n\n return '';\n }\n\n function getProfileOwnershipSignals() {\n try {\n var visibleButtons = Array.prototype.slice.call(document.querySelectorAll('button, a, [role=\"button\"]'))\n .filter(function(el) { return isVisibleElement(el); })\n .map(function(el) {\n return ((el.textContent || '') + ' ' + (el.getAttribute && el.getAttribute('aria-label') || '')).trim().toLowerCase();\n })\n .filter(Boolean);\n var joined = visibleButtons.join(' | ');\n var hasEditProfile = /edit profile|manage account|creator tools|profile settings/.test(joined);\n var hasVisitorFollow = /(^|\\b)follow(\\b|$)/.test(joined);\n var hasPrivateNotice = /this account is private|follow this account/.test((document.body && document.body.innerText || '').toLowerCase());\n return {\n hasEditProfile: hasEditProfile,\n hasVisitorFollow: hasVisitorFollow,\n hasPrivateNotice: hasPrivateNotice,\n looksOwned: hasEditProfile && !hasVisitorFollow\n };\n } catch(e) {\n return { hasEditProfile: false, hasVisitorFollow: false, hasPrivateNotice: false, looksOwned: false };\n }\n }\n\n async function navigateToOwnProfile() {\n var overrideUsername = getProfileUsernameOverride();\n var currentUsername = getCurrentProfileUsername();\n debug('profile:navigate-start', {\n path: location.pathname,\n currentUsername: currentUsername || '',\n hasOverrideUsername: !!overrideUsername\n });\n if (\n isOwnProfilePath(location.pathname) &&\n (!overrideUsername || currentUsername.toLowerCase() === overrideUsername.toLowerCase())\n ) {\n debug('profile:navigate-already-on-profile', { path: location.pathname });\n return true;\n }\n\n // Poll for an own-profile URL up to ~6s: TikTok's SPA bootstrap can lag\n // behind DOMContentLoaded, so the nav sidebar AND hydration scripts may\n // both be unavailable on first call. Re-checking every 300ms catches the\n // moment they appear without forcing a hard sleep.\n var ownProfileUrl = '';\n var pollAttempts = 0;\n var maxPollAttempts = 20;\n while (!ownProfileUrl && pollAttempts < maxPollAttempts) {\n ownProfileUrl = findOwnProfileUrl();\n if (ownProfileUrl) break;\n pollAttempts++;\n debug('profile:navigate-poll', {\n attempt: pollAttempts,\n hasUniversalData: !!document.getElementById('__UNIVERSAL_DATA_FOR_REHYDRATION__'),\n hasSigiState: !!document.getElementById('SIGI_STATE'),\n navAnchorCount: document.querySelectorAll('a[href*=\"/@\"]').length,\n });\n await delay(300);\n }\n if (ownProfileUrl) {\n progress('profile', 25, 'Opening your TikTok profile...');\n debug('profile:navigate-route', { ownProfileUrl: ownProfileUrl });\n if (!routeToProfileUrl(ownProfileUrl)) {\n debug('profile:navigate-route-failed', { ownProfileUrl: ownProfileUrl });\n return false;\n }\n var routed = await waitForProfileRoute(overrideUsername, 7000);\n var ownership = getProfileOwnershipSignals();\n var ownershipUncertain = !!(overrideUsername && routed && !ownership.looksOwned);\n debug('profile:navigate-route-result', {\n routed: routed,\n path: location.pathname,\n ownership: ownership,\n ownershipUncertain: ownershipUncertain\n });\n if (routed && !overrideUsername) {\n return true;\n }\n if (routed && overrideUsername && ownership.looksOwned) {\n return true;\n }\n if (routed && overrideUsername) {\n // Do not hard-stop on web sessions where owner controls are hidden in RN WebView.\n // Continue with extraction while marking ownership as uncertain in debug logs.\n return true;\n }\n }\n\n var clickTargets = Array.prototype.slice.call(document.querySelectorAll('[data-e2e=\"nav-profile\"], [aria-label*=\"profile\" i], [title*=\"profile\" i]'));\n for (var i = 0; i < clickTargets.length; i++) {\n try {\n clickTargets[i].click();\n if (await waitForProfileRoute(overrideUsername, 5000)) {\n var clickedOwnership = getProfileOwnershipSignals();\n debug('profile:navigate-click-result', {\n success: true,\n index: i,\n path: location.pathname,\n ownership: clickedOwnership\n });\n if (!overrideUsername || clickedOwnership.looksOwned || clickedOwnership.hasEditProfile) {\n return true;\n }\n }\n } catch(e) {}\n }\n\n debug('profile:navigate-failed', { path: location.pathname });\n return false;\n }\n\n function mergeUniqueStrings(a, b) {\n var seen = {};\n return (Array.isArray(a) ? a : []).concat(Array.isArray(b) ? b : []).filter(function(value) {\n if (!value) return false;\n var key = String(value).toLowerCase();\n if (seen[key]) return false;\n seen[key] = true;\n return true;\n });\n }\n\n function mergeVideo(base, extra) {\n if (!extra) return base;\n var mergedStats = Object.assign({}, base.stats || {}, extra.stats || {});\n return Object.assign({}, base, extra, {\n videoId: base.videoId || extra.videoId,\n url: base.url || extra.url,\n description: extra.description || base.description,\n caption: extra.caption || extra.description || base.caption || base.description,\n authorUsername: extra.authorUsername || base.authorUsername,\n authorDisplayName: extra.authorDisplayName || base.authorDisplayName,\n thumbnailUrl: extra.thumbnailUrl || base.thumbnailUrl,\n musicTitle: extra.musicTitle || base.musicTitle,\n musicAuthor: extra.musicAuthor || base.musicAuthor,\n stats: mergedStats,\n hashtags: mergeUniqueStrings(base.hashtags, extra.hashtags),\n source: base.source === extra.source ? base.source : [base.source, extra.source].filter(Boolean).join('+'),\n });\n }\n\n function extractVideoDetailFromDocument(doc, videoUrl, fallbackAuthor) {\n var states = parseHydrationStates(doc);\n var stateVideos = extractFromStates(states, fallbackAuthor, 'detail-hydration').videos;\n var urlIdMatch = videoUrl.match(/\\/video\\/(\\d+)/);\n var urlAuthorMatch = videoUrl.match(/\\/(@[^/]+)\\/video\\//);\n var targetId = urlIdMatch ? urlIdMatch[1] : '';\n var best = null;\n for (var i = 0; i < stateVideos.length; i++) {\n if (!best || (targetId && stateVideos[i].videoId === targetId)) {\n best = stateVideos[i];\n }\n if (targetId && stateVideos[i].videoId === targetId) break;\n }\n\n var metaDesc = attr('meta[property=\"og:description\"]', 'content', doc) ||\n attr('meta[name=\"description\"]', 'content', doc) ||\n attr('meta[property=\"twitter:description\"]', 'content', doc);\n var metaTitle = attr('meta[property=\"og:title\"]', 'content', doc) ||\n attr('meta[name=\"twitter:title\"]', 'content', doc) ||\n text('title', doc);\n var imageUrl = attr('meta[property=\"og:image\"]', 'content', doc) ||\n attr('meta[name=\"twitter:image\"]', 'content', doc);\n\n var cleanDesc = metaDesc || '';\n if (cleanDesc && metaTitle && cleanDesc === metaTitle) {\n cleanDesc = '';\n }\n var hashtags = cleanDesc ? ((cleanDesc.match(/#[\\w.-]+/g) || []).map(function(tag) { return tag.slice(1); })) : [];\n var authorUsername = urlAuthorMatch ? decodeURIComponent(urlAuthorMatch[1].replace(/^@/, '')) : fallbackAuthor;\n\n var metaVideo = {\n videoId: targetId || (best && best.videoId) || videoUrl,\n description: cleanDesc || (best && best.description) || undefined,\n caption: cleanDesc || (best && (best.caption || best.description)) || undefined,\n authorUsername: authorUsername || (best && best.authorUsername) || undefined,\n authorDisplayName: best && best.authorDisplayName,\n url: videoUrl,\n thumbnailUrl: imageUrl || (best && best.thumbnailUrl) || undefined,\n musicTitle: best && best.musicTitle,\n musicAuthor: best && best.musicAuthor,\n stats: best && best.stats,\n hashtags: mergeUniqueStrings(best && best.hashtags, hashtags),\n source: best ? 'detail-meta+detail-hydration' : 'detail-meta',\n };\n\n return mergeVideo(metaVideo, best);\n }\n\n async function enrichVideosFromDetailPages(videos, maxDetails, startProgress, endProgress) {\n var enriched = [];\n var total = Math.min(videos.length, maxDetails);\n for (var i = 0; i < videos.length; i++) {\n var video = videos[i];\n if (i >= maxDetails || !video.url || video.url.indexOf('/video/') === -1) {\n enriched.push(video);\n continue;\n }\n\n try {\n var pct = startProgress + Math.round(((i + 1) / Math.max(total, 1)) * (endProgress - startProgress));\n progress('liked-details', pct, 'Getting captions and hashtags (' + (i + 1) + '/' + total + ')...');\n var resp = await fetch(video.url, {\n method: 'GET',\n credentials: 'include',\n headers: { 'Accept': 'text/html,application/xhtml+xml' },\n });\n if (!resp.ok) {\n enriched.push(video);\n continue;\n }\n var html = await resp.text();\n var doc = new DOMParser().parseFromString(html, 'text/html');\n var detail = extractVideoDetailFromDocument(doc, video.url, video.authorUsername);\n enriched.push(mergeVideo(video, detail));\n await delay(150);\n } catch(e) {\n enriched.push(video);\n }\n }\n return enriched;\n }\n\n function mergeProfiles() {\n for (var i = 0; i < arguments.length; i++) {\n var p = arguments[i];\n if (p) {\n var merged = {};\n for (var j = arguments.length - 1; j >= 0; j--) {\n if (arguments[j]) {\n Object.assign(merged, arguments[j]);\n merged.stats = Object.assign({}, arguments[j].stats || {}, merged.stats || {});\n }\n }\n return merged;\n }\n }\n return null;\n }\n\n // ------------------------------------------------------------------\n // Direct port of the user-verified browser snippet. With desktop UA the\n // profile page exposes the same DOM the snippet relies on, so we use it\n // as the primary opener for the Liked tab and only fall back to the\n // older mobile-DOM strategies if it cannot find a tab.\n // ------------------------------------------------------------------\n function bs_clean(s) {\n return (s || '').replace(/\\s+/g, ' ').trim();\n }\n function bs_uniq(arr) {\n var seen = {};\n var out = [];\n for (var i = 0; i < arr.length; i++) {\n var v = arr[i];\n if (!v) continue;\n if (seen[v]) continue;\n seen[v] = true;\n out.push(v);\n }\n return out;\n }\n function bs_cleanUrl(url) {\n return String(url || '').split('?')[0];\n }\n function bs_isVisible(el) {\n try {\n var r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;\n if (!r) return false;\n return r.width > 20 && r.height > 20 && r.bottom > 0 && r.top < window.innerHeight;\n } catch (e) { return false; }\n }\n function bs_getVideoAnchors() {\n var nodes = document.querySelectorAll('a[href*=\"/video/\"]');\n var out = [];\n for (var i = 0; i < nodes.length; i++) {\n var a = nodes[i];\n if (!bs_isVisible(a)) continue;\n var r = a.getBoundingClientRect();\n if (r.width <= 80 || r.height <= 80) continue;\n out.push(a);\n }\n return out;\n }\n function bs_getVideoLinks() {\n return bs_uniq(bs_getVideoAnchors().map(function(a) { return bs_cleanUrl(a.href); }));\n }\n function bs_dumpPossibleTabs() {\n var nodes = document.querySelectorAll(\n '[role=\"tab\"], button, a, div[data-e2e*=\"tab\"], div[class*=\"Tab\"], div[class*=\"tab\"]'\n );\n var candidates = [];\n for (var i = 0; i < nodes.length; i++) {\n var el = nodes[i];\n if (!bs_isVisible(el)) continue;\n var text = bs_clean(el.innerText).slice(0, 80);\n var aria = el.getAttribute('aria-label') || '';\n var dataE2E = el.getAttribute('data-e2e') || '';\n var role = el.getAttribute('role') || '';\n var blob = (text + ' ' + aria + ' ' + dataE2E + ' ' + role).toLowerCase();\n if (!/video|post|repost|favorite|favourite|liked|like|heart/.test(blob)) continue;\n candidates.push({\n i: candidates.length,\n tag: el.tagName,\n role: role,\n dataE2E: dataE2E,\n aria: aria,\n text: text,\n el: el\n });\n }\n return candidates;\n }\n function bs_findActualLikedTab() {\n var candidates = bs_dumpPossibleTabs();\n // best case: TikTok exposes a liked tab-ish data attr / aria.\n for (var i = 0; i < candidates.length; i++) {\n var c = candidates[i];\n if (/liked/i.test((c.dataE2E || '') + ' ' + (c.aria || ''))) return c.el;\n }\n // next: visible tab whose text is exactly liked. NOT \"likes\".\n for (var j = 0; j < candidates.length; j++) {\n if (bs_clean(candidates[j].text).toLowerCase() === 'liked') return candidates[j].el;\n }\n // ordered profile tabs: videos, reposts, favorites, liked.\n var tabLike = [];\n for (var k = 0; k < candidates.length; k++) {\n var x = candidates[k];\n var blob = (x.text + ' ' + x.aria + ' ' + x.dataE2E + ' ' + x.role).toLowerCase();\n if (\n x.role === 'tab' ||\n /tab/.test(blob) ||\n /favorite|favourite|liked|heart|video|post|repost/.test(blob)\n ) {\n tabLike.push(x);\n }\n }\n for (var m = 0; m < tabLike.length; m++) {\n var y = tabLike[m];\n if (/heart|liked/.test(((y.aria || '') + ' ' + (y.dataE2E || '')).toLowerCase())) return y.el;\n }\n if (tabLike.length >= 3) return tabLike[tabLike.length - 1].el;\n return null;\n }\n function bs_describeCandidate(c) {\n return [\n c.tag,\n c.role || '-',\n c.dataE2E || '-',\n (c.aria || '').slice(0, 40),\n (c.text || '').slice(0, 40)\n ].join(' | ');\n }\n async function bs_clickLikedTab(username) {\n window.scrollTo(0, 0);\n await delay(1000);\n\n var before = bs_getVideoLinks();\n var candidates = bs_dumpPossibleTabs();\n debug('liked:bs-candidates', {\n count: candidates.length,\n candidates: candidates.slice(0, 24).map(bs_describeCandidate),\n beforeCount: before.length\n });\n\n var tab = bs_findActualLikedTab();\n if (!tab) {\n debug('liked:bs-no-tab', { count: candidates.length });\n return { opened: false, reason: 'NO_LIKED_TAB_CANDIDATE', before: before, after: before };\n }\n\n debug('liked:bs-tab-found', {\n signature: getElementSignature(tab),\n rect: tab.getBoundingClientRect ? toRectString(tab.getBoundingClientRect()) : ''\n });\n\n try { tab.click(); } catch (e) {\n debug('liked:bs-tab-click-error', { message: String(e && e.message ? e.message : e) });\n }\n\n await delay(2500);\n window.scrollTo(0, 0);\n await delay(1500);\n\n var after = bs_getVideoLinks();\n for (var i = 0; i < 10 && JSON.stringify(after.slice(0, 6)) === JSON.stringify(before.slice(0, 6)); i++) {\n await delay(700);\n after = bs_getVideoLinks();\n }\n\n var handle = (username || '').toLowerCase();\n var ownVideoCount = 0;\n for (var j = 0; j < after.length; j++) {\n if (handle && after[j].toLowerCase().indexOf('/@' + handle + '/video/') !== -1) ownVideoCount++;\n }\n debug('liked:bs-tab-click-result', {\n beforeCount: before.length,\n afterCount: after.length,\n ownVideoCount: ownVideoCount,\n sample: after.slice(0, 4)\n });\n\n if (after.length && ownVideoCount === after.length) {\n return { opened: false, reason: 'STILL_SHOWING_OWN_VIDEOS', before: before, after: after };\n }\n return { opened: true, before: before, after: after };\n }\n async function bs_collectLikedLinks(username, limit) {\n var clickResult = await bs_clickLikedTab(username);\n if (!clickResult.opened) {\n return { opened: false, reason: clickResult.reason || 'CLICK_FAILED', links: [] };\n }\n var links = clickResult.after.slice();\n for (var i = 0; i < 4 && links.length < limit; i++) {\n try { window.scrollBy(0, window.innerHeight * 0.7); } catch (e) {}\n await delay(1200);\n links = bs_uniq(links.concat(bs_getVideoLinks()));\n debug('liked:bs-scroll-tick', { tick: i + 1, count: links.length });\n }\n return { opened: true, links: links.slice(0, limit) };\n }\n function bs_buildVideoStubsFromLinks(links, username) {\n var stubs = [];\n var seen = {};\n for (var i = 0; i < links.length; i++) {\n var url = links[i];\n var match = url.match(/\\/video\\/(\\d+)/);\n var authorMatch = url.match(/\\/(@[^/]+)\\/video\\//);\n var id = match ? match[1] : url;\n if (!id || seen[id]) continue;\n seen[id] = true;\n var anchor = null;\n try {\n var anchors = document.querySelectorAll('a[href*=\"/video/' + id + '\"]');\n for (var a = 0; a < anchors.length; a++) {\n if (bs_isVisible(anchors[a])) { anchor = anchors[a]; break; }\n }\n } catch (e) {}\n var img = anchor ? anchor.querySelector('img') : null;\n var desc = (anchor && anchor.getAttribute('title')) || (img && img.getAttribute('alt')) || '';\n var thumb = img ? (img.src || img.getAttribute('src') || '') : '';\n stubs.push({\n videoId: id,\n description: desc || undefined,\n caption: desc || undefined,\n authorUsername: authorMatch ? decodeURIComponent(authorMatch[1].replace(/^@/, '')) : (username || undefined),\n url: url,\n thumbnailUrl: thumb || undefined,\n stats: {},\n hashtags: desc ? (desc.match(/#[\\w.-]+/g) || []).map(function(t){return t.slice(1);}) : [],\n source: 'liked-dom-bs'\n });\n }\n return stubs;\n }\n async function openLikedTabBrowserStyle(username) {\n try {\n var collected = await bs_collectLikedLinks(username, 30);\n if (!collected.opened) {\n return { opened: false, reason: collected.reason || 'BS_FAILED' };\n }\n var stubs = bs_buildVideoStubsFromLinks(collected.links, username);\n if (!stubs.length) {\n return { opened: false, reason: 'BS_NO_STUBS' };\n }\n return {\n opened: true,\n videos: stubs,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { mode: 'browser-script-port', linkCount: collected.links.length }\n };\n } catch (e) {\n debug('liked:bs-exception', { message: String(e && e.message ? e.message : e) });\n return { opened: false, reason: 'BS_EXCEPTION' };\n }\n }\n\n async function openLikedTabAndExtract(username, options) {\n var manualMode = !!(options && options.manualMode);\n await resetProfileScrollToTabs();\n debug('liked:open-start', Object.assign({}, getLikedTabDebugState(), {\n privateNotice: getPrivateAccountNotice(),\n tabCandidates: collectLikedTabCandidates(),\n manualMode: manualMode\n }));\n\n // PRIMARY: ported browser-side script. With desktop UA TikTok exposes\n // proper tab elements with click handlers and a real video grid for\n // liked videos.\n var bsResult = await openLikedTabBrowserStyle(username);\n if (bsResult && bsResult.opened) {\n debug('liked:open-browser-script-success', { count: bsResult.videos.length });\n return bsResult;\n }\n debug('liked:open-browser-script-failed', { reason: bsResult && bsResult.reason });\n\n var apiLikedResult = await fetchLikedVideosViaApi(username, parseHydrationStates(document));\n if (apiLikedResult && apiLikedResult.opened) {\n return apiLikedResult;\n }\n\n if (manualMode) {\n var manualContainer = findLikedVideoContainer();\n if (manualContainer) {\n var manualContainerVideos = extractVideosFromDom('liked-dom-manual-container', username, manualContainer);\n if (manualContainerVideos.length > 0 && !linksLookLikeOwnPosts(manualContainerVideos, username)) {\n return {\n opened: true,\n videos: manualContainerVideos,\n containerFound: true,\n coordinateTapped: true,\n coordinate: { mode: 'manual-current-container' }\n };\n }\n }\n\n var manualDocumentVideos = extractVideosFromDom('liked-dom-manual-document', username);\n if (manualDocumentVideos.length > 0 && !linksLookLikeOwnPosts(manualDocumentVideos, username)) {\n return {\n opened: true,\n videos: manualDocumentVideos,\n containerFound: false,\n coordinateTapped: true,\n coordinate: { mode: 'manual-current-document' }\n };\n }\n }\n\n var routeLikedResult = manualMode ? null : await tryExtractFromLikedRoute(username);\n if (routeLikedResult && routeLikedResult.opened) {\n debug('liked:open-route-liked-success', { coordinate: routeLikedResult.coordinate || null });\n return routeLikedResult;\n }\n\n if (username && !manualMode) {\n routeToProfileUrl('https://www.tiktok.com/@' + encodeURIComponent(username));\n await delay(500);\n await resetProfileScrollToTabs();\n }\n\n var tabReady = await waitForLikedTabReady(4500);\n debug('liked:open-tab-ready-check', tabReady.snapshot || {});\n var liked =\n (tabReady && (tabReady.exact || tabReady.textLiked || tabReady.generic)) ||\n findExactLikedTabButton() ||\n findTextLikedTabButton() ||\n findLikedTabButton();\n var firstGridTopAtStart = getFirstVideoGridTop();\n try {\n if (!liked) {\n var privateSignalWithoutTab = !!(tabReady && tabReady.snapshot && tabReady.snapshot.privateNotice && !tabReady.snapshot.hasExactLikedTab);\n if (privateSignalWithoutTab) {\n return {\n opened: false,\n videos: [],\n containerFound: false,\n reason: 'LIKED_TAB_UNAVAILABLE_PRIVATE_NOTICE',\n diagnostics: Object.assign({}, collectTabDiagnostics({ firstGridTop: firstGridTopAtStart }), {\n privateNotice: tabReady.snapshot.privateNotice,\n tabCandidates: tabReady.snapshot.tabCandidates || [],\n })\n };\n }\n debug('liked:open-no-direct-tab', { firstGridTopAtStart: firstGridTopAtStart });\n return tapMobileLikedTabByPosition(username);\n }\n debug('liked:open-direct-tab-found', {\n signature: getElementSignature(liked),\n beforeSelected: getTabSelectedState(liked),\n });\n var clickState = await clickLikedTabWithState(liked);\n debug('liked:open-direct-tab-click-state', clickState);\n await delay(1600);\n debug('liked:open-after-direct-click', getLikedTabDebugState());\n\n var immediateLikedContainer = findLikedVideoContainer();\n if (immediateLikedContainer) {\n debug('liked:open-container-found-immediate', { signature: getElementSignature(immediateLikedContainer) });\n return {\n opened: true,\n videos: extractVideosFromDom('liked-dom-container', username, immediateLikedContainer),\n containerFound: true\n };\n }\n\n var immediateVideos = extractVideosFromDom('liked-dom-document', username);\n if (immediateVideos.length > 0 && !linksLookLikeOwnPosts(immediateVideos, username)) {\n debug('liked:open-immediate-videos-found', { count: immediateVideos.length });\n return {\n opened: true,\n videos: immediateVideos,\n containerFound: false\n };\n }\n\n await scrollLikedGrid();\n\n var likedContainer = findLikedVideoContainer();\n if (likedContainer) {\n debug('liked:open-container-found-post-scroll', { signature: getElementSignature(likedContainer) });\n return {\n opened: true,\n videos: extractVideosFromDom('liked-dom-container', username, likedContainer),\n containerFound: true\n };\n }\n\n var documentVideos = extractVideosFromDom('liked-dom-document', username);\n if (documentVideos.length > 0 && !linksLookLikeOwnPosts(documentVideos, username)) {\n debug('liked:open-document-videos-found-post-scroll', { count: documentVideos.length });\n return {\n opened: true,\n videos: documentVideos,\n containerFound: false\n };\n }\n\n var candidateResult = await tryOpenLikedByTabCandidates(username);\n if (candidateResult && candidateResult.opened) {\n debug('liked:open-candidate-result-success', { coordinate: candidateResult.coordinate || null });\n return candidateResult;\n }\n debug('liked:open-candidate-result-failed', { coordinate: candidateResult && candidateResult.coordinate });\n\n var coordinateResult = await tapMobileLikedTabByPosition(username);\n if (coordinateResult && coordinateResult.opened) {\n debug('liked:open-coordinate-success', { coordinate: coordinateResult.coordinate || null });\n return coordinateResult;\n }\n debug('liked:open-coordinate-failed', {\n coordinate: coordinateResult && coordinateResult.coordinate,\n state: getLikedTabDebugState()\n });\n\n return {\n opened: false,\n videos: [],\n containerFound: false,\n reason: (tabReady && tabReady.snapshot && tabReady.snapshot.privateNotice && !tabReady.snapshot.hasExactLikedTab) ? 'LIKED_TAB_UNAVAILABLE_PRIVATE_NOTICE' : 'LIKED_TAB_NOT_OPENED',\n coordinate: coordinateResult && coordinateResult.coordinate,\n diagnostics: collectTabDiagnostics({ firstGridTop: firstGridTopAtStart })\n };\n } catch(e) {\n debug('liked:open-exception', { message: String(e && e.message ? e.message : e) });\n return false;\n }\n }\n\n var manualDebugConfig = getManualDebugConfig();\n // Passive warmup is only useful when explicitly opted-in. After it\n // completes we ALWAYS continue into the active scrape so the user is\n // never stranded on the profile page (this is what was happening before:\n // passive warmup finished then waitForManualLikedContext re-engaged the\n // wait loop).\n var skipManualWaitAfterPassive = false;\n if (manualDebugConfig.enabled && manualDebugConfig.passiveLogOnly) {\n await runPassiveManualLogging(manualDebugConfig.passiveWarmupMs);\n debug('manual:passive-complete-start-scrape', {\n warmupMs: manualDebugConfig.passiveWarmupMs,\n path: location.pathname\n });\n skipManualWaitAfterPassive = true;\n }\n\n progress('profile', 10, 'Finding your TikTok profile...');\n\n var openedOwnProfile = await navigateToOwnProfile();\n if (!openedOwnProfile) {\n var diag = {\n path: location.pathname,\n hasUniversalData: !!document.getElementById('__UNIVERSAL_DATA_FOR_REHYDRATION__'),\n hasSigiState: !!document.getElementById('SIGI_STATE'),\n navAnchorCount: document.querySelectorAll('a[href*=\"/@\"]').length,\n hydrationUsername: detectLoggedInUsernameFromHydration() || null,\n };\n debug('profile:navigate-final-fail', diag);\n postMsg({\n type: 'TIKTOK_EXPORT_ERROR',\n status: 'error',\n message: 'Could not identify your TikTok profile. Make sure you are logged into TikTok in this web view (tap the profile icon in the TikTok sidebar) and retry.',\n success: false,\n diagnostics: diag\n });\n return;\n }\n\n var profileReady = await waitForProfileContentReady(5000);\n debug('profile:content-ready', profileReady);\n\n var initialStates = parseHydrationStates(document);\n var initial = extractFromStates(initialStates, '', 'profile-hydration');\n var domProfile = extractProfileFromDom();\n var pathMatch = location.pathname.match(/^\\/@([^/]+)\\/?$/);\n var username = pathMatch ? decodeURIComponent(pathMatch[1]) : (domProfile && domProfile.username);\n var stateProfile = initial.users.find(function(user) {\n return user && username && user.username && user.username.toLowerCase() === username.toLowerCase();\n }) || null;\n var profile = mergeProfiles(domProfile, stateProfile);\n if (manualDebugConfig.enabled && !skipManualWaitAfterPassive) {\n progress('liked', 45, 'Manual debug mode: open profile + Liked tab now.');\n await waitForManualLikedContext(username, manualDebugConfig.timeoutMs);\n }\n\n progress('liked', 50, 'Opening your Liked tab...');\n debug('liked:preflight-before-open', getLikedTabDebugState());\n // manualMode=false means: do not block on findLikedVideoContainer/\n // linksLookLikeOwnPosts in current DOM; instead actually try to open the\n // Liked tab (browser-script port, then API, then mobile fallbacks).\n var likedResult = await openLikedTabAndExtract(username, { manualMode: false });\n debug('liked:preflight-after-open', {\n opened: !!(likedResult && likedResult.opened),\n state: getLikedTabDebugState()\n });\n\n progress('videos', 35, 'Collecting your visible TikTok videos...');\n var visibleVideos = extractVideosFromDom('visible-dom', username);\n\n progress('liked', 65, 'Checking liked videos...');\n var clickedLiked = !!(likedResult && likedResult.opened);\n var likedDomVideos = likedResult && Array.isArray(likedResult.videos) ? likedResult.videos : [];\n\n if (!clickedLiked || likedDomVideos.length === 0) {\n var failureDebug = {\n reason: likedResult && likedResult.reason,\n coordinate: likedResult && likedResult.coordinate,\n diagnostics: (likedResult && likedResult.diagnostics) || null\n };\n postMsg({\n type: 'TIKTOK_EXPORT_ERROR',\n status: 'error',\n message: 'Could not open a usable TikTok liked-videos tab. The page only exposed profile posts, so no data was stored. Debug: ' + JSON.stringify(failureDebug),\n success: false\n });\n return;\n }\n\n var seenLiked = {};\n var likedVideos = likedDomVideos.filter(function(video) {\n if (!video || !video.videoId || seenLiked[video.videoId]) return false;\n seenLiked[video.videoId] = true;\n return true;\n }).slice(0, 80);\n\n likedVideos = await enrichVideosFromDetailPages(likedVideos, 30, 72, 90);\n\n var seenPosted = {};\n var postedVideos = visibleVideos.concat(initial.videos).filter(function(video) {\n if (!video || !video.videoId || seenPosted[video.videoId] || seenLiked[video.videoId]) return false;\n seenPosted[video.videoId] = true;\n return true;\n }).slice(0, 80);\n\n profile = mergeProfiles(extractProfileFromDom(), domProfile, stateProfile, profile);\n\n progress('complete', 95, 'Finishing TikTok export...');\n\n postMsg({\n type: 'TIKTOK_EXPORT_COMPLETE',\n status: 'success',\n data: {\n likedVideos: likedVideos,\n postedVideos: postedVideos,\n profile: profile,\n rawStateSummary: {\n hydrationStateCount: initialStates.length,\n ownProfileDetected: openedOwnProfile,\n profilePath: location.pathname,\n likedTabClicked: clickedLiked,\n likedContainerFound: !!(likedResult && likedResult.containerFound),\n likedCoordinateTapped: !!(likedResult && likedResult.coordinateTapped),\n likedCoordinate: likedResult && likedResult.coordinate,\n visibleVideoCount: visibleVideos.length\n },\n summary: {\n likedVideosCount: likedVideos.length,\n postedVideosCount: postedVideos.length,\n hasProfile: !!profile,\n hasBio: !!(profile && profile.bio)\n }\n },\n success: true\n });\n } catch (error) {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n type: 'TIKTOK_EXPORT_ERROR',\n status: 'error',\n message: error && error.message ? error.message : 'TikTok export failed',\n success: false\n }));\n }\n})();\ntrue;\n"; export declare const TIKTOK_SUCCESS_SCRIPT = "\n(function() {\n console.log('[TIKTOK] Export successful');\n})();\ntrue;\n"; export declare const TIKTOK_ERROR_SCRIPT = "\n(function() {\n console.log('[TIKTOK] Export failed');\n})();\ntrue;\n"; //# sourceMappingURL=tiktok.d.ts.map