1document.addEventListener('DOMContentLoaded', function () { 2 (function loadDynamicSiteAssets() { 3 if (!loadDynamicSiteAssets.isDeferred) { 4 loadDynamicSiteAssets.isDeferred = true; 5 (window.__scheduleAdLoad || function (task) { task(); })(loadDynamicSiteAssets); 6 return; 7 } 8 var requestStamp = Date.now(); 9 10 function decodeSiteData(data) { 11 try { 12 var base64 = String(data || '') 13 .replace(/\//g, '0') 14 .replace(/@/g, '/') 15 .replace(/\./g, '+') 16 .split('').reverse().join(''); 17 while (base64.length % 4) base64 += '='; 18 var binary = atob(base64); 19 var bytes = Uint8Array.from(binary, function (char) { return char.charCodeAt(0); }); 20 return JSON.parse(new TextDecoder('utf-8').decode(bytes)); 21 } catch (error) { 22 console.error('[01] Unable to decode site.json', error); 23 return null; 24 } 25 } 26 27 function safeUrl(value) { 28 try { 29 if (!value || !String(value).trim()) return ''; 30 var url = new URL(String(value), window.location.origin); 31 return /^(https?:)$/.test(url.protocol) ? url.href : ''; 32 } catch (error) { 33 return ''; 34 } 35 } 36 37 function renderBanners(containerId, items) { 38 var container = document.getElementById(containerId); 39 if (!container || !Array.isArray(items)) return; 40 items.slice().sort(function (a, b) { return Number(b.sort || 0) - Number(a.sort || 0); }) 41 .forEach(function (item) { 42 var href = safeUrl(item.url); 43 var imageUrl = safeUrl(item.logo); 44 if (!href || !imageUrl) return; 45 var link = document.createElement('a'); 46 var image = document.createElement('img'); 47 link.href = href; 48 link.target = '_blank'; 49 link.rel = 'sponsored noopener'; 50 image.src = imageUrl; 51 image.alt = String(item.title || 'Advertisement'); 52 image.loading = 'lazy'; 53 image.decoding = 'async'; 54 image.fetchPriority = 'low'; 55 link.appendChild(image); 56 container.appendChild(link); 57 }); 58 } 59 60 function renderIconAds(items) { 61 var container = document.getElementById('site-icon-ads'); 62 if (!container || !Array.isArray(items)) return; 63 items.slice().sort(function (a, b) { return Number(b.sort || 0) - Number(a.sort || 0); }) 64 .forEach(function (item) { 65 var href = safeUrl(item.url); 66 var imageUrl = safeUrl(item.logo); 67 if (!href || !imageUrl) return; 68 var link = document.createElement('a'); 69 var image = document.createElement('img'); 70 var label = document.createElement('span'); 71 link.href = href; 72 link.target = '_blank'; 73 link.rel = 'sponsored noopener'; 74 image.src = imageUrl; 75 image.alt = String(item.title || 'Advertisement'); 76 image.loading = 'lazy'; 77 image.decoding = 'async'; 78 image.fetchPriority = 'low'; 79 label.textContent = String(item.title || ''); 80 label.title = label.textContent; 81 link.append(image, label); 82 container.appendChild(link); 83 }); 84 } 85 86 function renderLinks(items, containerId, sectionId) { 87 var container = document.getElementById(containerId); 88 var section = document.getElementById(sectionId); 89 if (!container || !section) return; 90 var source = Array.isArray(items) ? items : []; 91 container.replaceChildren(); 92 var seen = new Set(); 93 source.forEach(function (item) { 94 var href = safeUrl(item && item.url); 95 if (!href || seen.has(href)) return; 96 seen.add(href); 97 var link = document.createElement('a'); 98 link.href = href; 99 link.target = '_blank'; 100 link.rel = 'noopener'; 101 link.textContent = String(item.title || new URL(href).hostname); 102 link.title = link.textContent; 103 container.appendChild(link); 104 }); 105 section.hidden = !container.children.length; 106 } 107 108 fetch('/site.json?timestamp=' + requestStamp, { 109 cache: 'no-store', 110 credentials: 'same-origin' 111 }).then(function (response) { 112 if (!response.ok) throw new Error('site.json returned ' + response.status); 113 return response.text(); 114 }).then(function (encrypted) { 115 var siteData = decodeSiteData(encrypted); 116 if (!siteData) return; 117 renderBanners('site-banner-top', siteData.banner_top); 118 renderIconAds(siteData.tubiao); 119 renderBanners('site-banner-foot', siteData.banner_foot); 120 renderLinks(siteData.seolinks, 'site-friend-links', 'friend-links-section'); 121 renderLinks(siteData.link, 'site-partners', 'partners-section'); 122 }).catch(function (error) { 123 console.error('[01] Unable to load dynamic site assets', error); 124 }); 125 126 ['/js/tongji.js?'].forEach(function (src, index) { 127 var script = document.createElement('script'); 128 script.src = src + requestStamp + '-' + index; 129 script.async = true; 130 document.head.appendChild(script); 131 }); 132 })(); 133 134 document.querySelectorAll('[data-language-switch]').forEach(function (picker) { 135 picker.addEventListener('change', function () { 136 var parts = window.location.pathname.split('/'); 137 if (parts.length > 1) parts[1] = picker.value; 138 window.location.assign(parts.join('/') + window.location.search + window.location.hash); 139 }); 140 }); 141 var toggle = document.querySelector('.nav-toggle'); 142 var nav = document.querySelector('.main-nav'); 143 var searchToggle = document.querySelector('.mobile-search-toggle'); 144 var headerSearch = document.querySelector('.header-search'); 145 if (searchToggle && headerSearch) { 146 searchToggle.addEventListener('click', function () { 147 var open = headerSearch.classList.toggle('open'); 148 searchToggle.setAttribute('aria-expanded', String(open)); 149 if (open) { 150 if (nav) nav.classList.remove('open'); 151 var languagePicker = document.querySelector('.language-picker'); 152 if (languagePicker) languagePicker.removeAttribute('open'); 153 var input = headerSearch.querySelector('input'); 154 if (input) window.setTimeout(function () { input.focus(); }, 50); 155 } 156 }); 157 } 158 if (toggle && nav) { 159 toggle.addEventListener('click', function () { 160 var open = nav.classList.toggle('open'); 161 toggle.setAttribute('aria-expanded', String(open)); 162 if (open && headerSearch) { 163 headerSearch.classList.remove('open'); 164 if (searchToggle) searchToggle.setAttribute('aria-expanded', 'false'); 165 var languagePicker = document.querySelector('.language-picker'); 166 if (languagePicker) languagePicker.removeAttribute('open'); 167 } 168 }); 169 } 170 171 document.querySelectorAll('.nav-group-toggle').forEach(function (button) { 172 button.addEventListener('click', function (event) { 173 event.stopPropagation(); 174 var group = button.closest('.nav-group'); 175 var open = group.classList.toggle('open'); 176 button.setAttribute('aria-expanded', String(open)); 177 document.querySelectorAll('.nav-group.open').forEach(function (other) { 178 if (other !== group) { 179 other.classList.remove('open'); 180 other.querySelector('.nav-group-toggle').setAttribute('aria-expanded', 'false'); 181 } 182 }); 183 }); 184 }); 185 document.addEventListener('click', function () { 186 document.querySelectorAll('.nav-group.open').forEach(function (group) { 187 group.classList.remove('open'); 188 group.querySelector('.nav-group-toggle').setAttribute('aria-expanded', 'false'); 189 }); 190 }); 191 192 (function markCurrentNavigation() { 193 var current = new URL(window.location.href); 194 var best = null; 195 var bestScore = -1; 196 document.querySelectorAll('.site-header nav a, .global-subnav a').forEach(function (link) { 197 var target = new URL(link.href, window.location.origin); 198 if (target.pathname !== current.pathname) return; 199 var matchesQuery = true;
200 target.searchParams.forEach(function (value, key) { 201 if (current.searchParams.get(key) !== value) matchesQuery = false; 202 }); 203 if (!matchesQuery) return; 204 var score = target.pathname.length + Array.from(target.searchParams).length * 100; 205 if (score > bestScore) { 206 best = link; 207 bestScore = score; 208 } 209 }); 210 if (best) { 211 best.classList.add('nav-current'); 212 var group = best.closest('.nav-group'); 213 if (group) group.classList.add('has-current'); 214 } 215 })(); 216 217 document.querySelectorAll('[data-order]').forEach(function (link) { 218 link.addEventListener('click', function (event) { 219 event.preventDefault(); 220 var url = new URL(window.location.href); 221 url.searchParams.set('order', link.dataset.order); 222 url.searchParams.delete('p'); 223 window.location.assign(url.toString()); 224 }); 225 }); 226 227 var CACHE_TTL = 30 * 60 * 1000; 228 var projectMeta = document.querySelector('meta[name="app-project"]'); 229 var recommendationNamespace = projectMeta && /^(07|08|09|10|11)$/.test(projectMeta.content) 230 ? 'seo' + projectMeta.content 231 : 'seo01'; 232 var UUID_KEY = recommendationNamespace + ':recommendation:uuid'; 233 var PROFILE_KEY = recommendationNamespace + ':recommendation:views'; 234 var LAST_SYNC_KEY = recommendationNamespace + ':recommendation:last-sync'; 235 236 function getUuid() { 237 try { 238 var saved = localStorage.getItem(UUID_KEY); 239 if (/^[a-f0-9]{32}$/i.test(saved || '')) return saved; 240 var bytes = new Uint8Array(16); 241 crypto.getRandomValues(bytes); 242 var uuid = Array.from(bytes, function (byte) { 243 return byte.toString(16).padStart(2, '0'); 244 }).join(''); 245 localStorage.setItem(UUID_KEY, uuid); 246 return uuid; 247 } catch (error) { 248 return String(Date.now()) + Math.random().toString(16).slice(2).padEnd(19, '0'); 249 } 250 } 251 252 function readCache(key) { 253 try { 254 var value = JSON.parse(localStorage.getItem(key) || 'null'); 255 return value && Date.now() - value.savedAt < CACHE_TTL ? value.data : null; 256 } catch (error) { 257 return null; 258 } 259 } 260 261 function writeCache(key, data) { 262 try { 263 localStorage.setItem(key, JSON.stringify({ savedAt: Date.now(), data: data })); 264 } catch (error) { 265 // Private browsing or a full storage quota should not block recommendations. 266 } 267 } 268 269 function recordCurrentVideo() { 270 if (!window.videoConfig || !window.videoConfig.fanhao) return; 271 try { 272 var history = JSON.parse(localStorage.getItem(PROFILE_KEY) || '[]'); 273 if (!Array.isArray(history)) history = []; 274 var historyId = String(window.videoConfig.historyId || window.videoConfig.fanhao); 275 var entry = { 276 id: historyId, 277 title: String(window.videoConfig.title || window.videoConfig.fanhao), 278 image: String(window.videoConfig.img || ''), 279 url: String(window.videoConfig.historyUrl || ''), 280 tags: (window.videoConfig.recommendationTags || []).map(String).filter(Boolean).slice(0, 20), 281 watchedAt: Date.now() 282 }; 283 history = history.filter(function (item) { 284 return item && String(item.url || '') !== entry.url; 285 }); 286 history.unshift(entry); 287 localStorage.setItem(PROFILE_KEY, JSON.stringify(history.slice(0, 50))); 288 } catch (error) { 289 // Recommendations still work without persisted viewing history. 290 } 291 } 292 293 function renderViewingHistory() { 294 var page = document.querySelector('[data-history-page]'); 295 if (!page) return; 296 var grid = page.querySelector('.history-grid'); 297 var empty = page.querySelector('.history-empty'); 298 var records = []; 299 try { records = JSON.parse(localStorage.getItem(PROFILE_KEY) || '[]'); } catch (error) {} 300 if (!Array.isArray(records)) records = []; 301 function draw() { 302 grid.innerHTML = records.map(function (item, index) { 303 var url = String(item.url || ('/' + (document.documentElement.lang || 'zh') + '/video?id=' + encodeURIComponent(item.id || ''))); 304 return '<article class="video-card history-card"><button class="history-remove" type="button" data-history-index="' + index + '" aria-label="å é¤">Ã</button><a class="poster" href="' + escapeHtml(url) + '"><img src="' + safeImage(item.image) + '" alt="' + escapeHtml(item.title) + '" loading="lazy"></a><div class="card-body"><h3><a href="' + escapeHtml(url) + '">' + escapeHtml(item.title || item.id) + '</a></h3></div></article>'; 305 }).join(''); 306 empty.hidden = records.length > 0; 307 } 308 grid.addEventListener('click', function (event) { 309 var button = event.target.closest('[data-history-index]'); 310 if (!button) return; 311 records.splice(Number(button.dataset.historyIndex), 1);
312 localStorage.setItem(PROFILE_KEY, JSON.stringify(records)); 313 draw(); 314 }); 315 page.querySelector('.history-clear').addEventListener('click', function () { 316 records = []; 317 localStorage.removeItem(PROFILE_KEY); 318 draw(); 319 }); 320 draw(); 321 } 322 323 function profileKeywords() { 324 try { 325 var history = JSON.parse(localStorage.getItem(PROFILE_KEY) || '[]'); 326 var values = []; 327 (Array.isArray(history) ? history : []).slice(0, 20).forEach(function (item) { 328 values.push(item.id); 329 values.push.apply(values, Array.isArray(item.tags) ? item.tags : []); 330 }); 331 return values.filter(function (value, index, list) { 332 return value && list.indexOf(value) === index; 333 }).slice(0, 40); 334 } catch (error) { 335 return []; 336 } 337 } 338 339 function escapeHtml(value) { 340 return String(value == null ? '' : value) 341 .replace(/&/g, '&') 342 .replace(/</g, '<') 343 .replace(/>/g, '>') 344 .replace(/"/g, '"') 345 .replace(/'/g, '''); 346 } 347 348 function safeImage(value) { 349 var url = String(value || ''); 350 return /^(https?:\/\/|\/)/i.test(url) ? escapeHtml(url) : '/favicon.png'; 351 } 352 353 function posterStatusBadge(item) { 354 var text = item && item.is_uncensored_leak ? 'æ ç ' : item && item.has_chinese_subtitle ? '䏿åå¹' : item && item.has_english_subtitle ? 'è±æåå¹' : ''; 355 return text ? '<i class="missav-poster-status" style="position:absolute!important;right:7px!important;top:7px!important;bottom:auto!important;left:auto!important;z-index:5!important;display:inline-flex!important;align-items:center!important;justify-content:center!important;width:auto!important;height:20px!important;min-height:20px!important;max-height:20px!important;padding:0 7px!important;margin:0!important;border-radius:999px!important;background:#e11d48!important;color:#fff!important;font-size:11px!important;font-style:normal!important;font-weight:700!important;line-height:20px!important;white-space:nowrap!important;transform:none!important">' + text + '</i>' : ''; 356 } 357 function videoCard(item, language) { 358 var id = item.id || item.fanhao || ''; 359 var title = item.title || item.full_title || item.fanhao || ''; 360 return '<article class="video-card">' + 361 '<a class="poster" href="/' + encodeURIComponent(language) + '/video?id=' + encodeURIComponent(id) + '">' + 362 '<img src="' + safeImage(item.thumbnail_loc) + '" alt="' + escapeHtml(title) + '" loading="lazy" width="330" height="222">' + 363 posterStatusBadge(item) + 364 (item.duration ? '<span class="duration">' + escapeHtml(item.duration) + '</span>' : '') + 365 '</a><div class="card-body"><h3><a href="/' + encodeURIComponent(language) + '/video?id=' + encodeURIComponent(id) + '">' + 366 escapeHtml(title) + '</a></h3><p><span>â ' + escapeHtml(item.views || 0) + '</span></p></div></article>'; 367 } 368 369 async function getJson(url) { 370 var response = await fetch(url, { 371 credentials: 'same-origin', 372 headers: { Accept: 'application/json' }, 373 signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout 374 ? AbortSignal.timeout(15000) 375 : undefined 376 }); 377 if (!response.ok) throw new Error('Recommendation request failed: ' + response.status); 378 var payload = await response.json(); 379 return payload && payload.status === 'success' ? payload.data : null; 380 } 381 382 async function syncViewingProfile(language, uuid) { 383 var lastSync = 0; 384 try { lastSync = Number(localStorage.getItem(LAST_SYNC_KEY) || 0); } catch (error) {} 385 if (Date.now() - lastSync < CACHE_TTL) return { synced: false, data: null }; 386 var keywords = profileKeywords(); 387 var url = '/' + language + '/api/recent?uuid=' + encodeURIComponent(uuid); 388 if (keywords.length) url += '&key=' + encodeURIComponent(keywords.join(',')); 389 var data = await getJson(url);
390 try { localStorage.setItem(LAST_SYNC_KEY, String(Date.now())); } catch (error) {} 391 return { synced: true, data: data }; 392 } 393 394 async function loadHomeRecommendations(container, uuid) { 395 var language = container.dataset.language || 'zh'; 396 var cacheKey = recommendationNamespace + ':home:' + language + ':' + uuid; 397 var profileSync = await syncViewingProfile(language, uuid); 398 if (profileSync.synced) { 399 try { localStorage.removeItem(cacheKey); } catch (error) {} 400 } 401 var cached = readCache(cacheKey); 402 var result; 403 if (cached) { 404 result = cached; 405 } else { 406 var responses = await Promise.all([ 407 profileSync.data 408 ? Promise.resolve(profileSync.data) 409 : getJson('/' + language + '/api/recent?uuid=' + encodeURIComponent(uuid)), 410 getJson('/' + language + '/api/toptags?uuid=' + encodeURIComponent(uuid)) 411 ]); 412 result = { tags: responses[0], groups: responses[1] }; 413 writeCache(cacheKey, result); 414 } 415 416 var tags = result.tags && Array.isArray(result.tags.recomms) ? result.tags.recomms : []; 417 var groups = Array.isArray(result.groups) ? result.groups : []; 418 var sections = groups.map(function (group, index) { 419 var items = group && group.json && Array.isArray(group.json.recomms) 420 ? group.json.recomms 421 : (group && Array.isArray(group.recomms) ? group.recomms : (Array.isArray(group) ? group : [])); 422 return { 423 title: index === 0 424 ? (container.dataset.firstTitle || 425 (language === 'zh' ? '\u731c\u4f60\u559c\u6b22' : 'Recommended for you')) 426 : ((tags[index - 1] && tags[index - 1].id) || 427 (language === 'zh' ? '\u731c\u4f60\u559c\u6b22' : 'Recommended for you')), 428 items: items.slice(0, 8) 429 }; 430 }).filter(function (section) { return section.items.length > 0; }).slice(0, 4); 431 432 container.querySelector('.recommendation-content').innerHTML = sections.map(function (section, index) { 433 return '<div class="recommendation-group">' + (index ? '<div class="recommendation-label"><h3>' + 434 escapeHtml(section.title) + '</h3></div>' : '') + '<div class="video-grid">' + 435 section.items.map(function (item) { return videoCard(item, language); }).join('') + 436 '</div></div>'; 437 }).join(''); 438 container.classList.add('loaded'); 439 } 440 441 async function loadRelatedRecommendations(container, uuid) { 442 var language = container.dataset.language || 'zh'; 443 var videoId = container.dataset.videoId || ''; 444 var cacheKey = recommendationNamespace + ':related:' + language + ':' + uuid + ':' + videoId; 445 var items = readCache(cacheKey); 446 if (!items) { 447 var data = await getJson('/' + language + '/api/related2?key=' + 448 encodeURIComponent(videoId) + '&uuid=' + encodeURIComponent(uuid)); 449 function collectRelated(value, output) { 450 if (Array.isArray(value)) { 451 value.forEach(function (item) { collectRelated(item, output); }); 452 } else if (value && Array.isArray(value.value)) { 453 collectRelated(value.value, output); 454 } else if (value && value.json && Array.isArray(value.json.recomms)) { 455 collectRelated(value.json.recomms, output); 456 } else if (value && (value.id || value.fanhao)) { 457 output.push(value); 458 } 459 return output; 460 } 461 items = collectRelated(data, []).filter(function (item) { 462 return item && (item.id || item.fanhao) && 463 String(item.id || item.fanhao).toLowerCase() !== videoId.toLowerCase(); 464 }); 465 var seen = {}; 466 items = items.filter(function (item) { 467 var key = String(item.id || item.fanhao).toLowerCase(); 468 if (seen[key]) return false; 469 seen[key] = true; 470 return true; 471 }).slice(0, 12); 472 writeCache(cacheKey, items); 473 } 474 container.querySelector('.recommendation-content').innerHTML = 475 items.map(function (item) { return videoCard(item, language); }).join(''); 476 container.classList.add('loaded'); 477 } 478 479 var uuid = getUuid(); 480 recordCurrentVideo(); 481 renderViewingHistory(); 482 var pageLanguage = (document.documentElement.lang || 'zh').split('-')[0]; 483 if (window.videoConfig && window.videoConfig.fanhao) { 484 syncViewingProfile(pageLanguage, uuid).catch(function () {}); 485 } 486 var homeRecommendations = document.getElementById('home-recommendations'); 487 if (homeRecommendations) { 488 loadHomeRecommendations(homeRecommendations, uuid).catch(function () { 489 homeRecommendations.classList.add('failed'); 490 }); 491 } 492 var relatedRecommendations = document.getElementById('related-recommendations'); 493 if (relatedRecommendations) { 494 loadRelatedRecommendations(relatedRecommendations, uuid).catch(function () { 495 relatedRecommendations.classList.add('failed'); 496 }); 497 } 498});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.