PageSourceSearch

https://blackedublog.com/air13-static/js/air13-app.js?v=202609061301

js blackedublog.com collected 2026-10-05 18:20:17 UTC 19,747 bytes, 465 lines download raw bytes

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

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.