NodeSeek <-> DeepFlood 联合访问

Visit nodeseek.com and deepflood.com at the same time and push hot posts

目前為 2025-09-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         NodeSeek <-> DeepFlood 联合访问
// @namespace    http://tampermonkey.net/
// @license      AGPL-3.0
// @version      2025-09-28
// @description  Visit nodeseek.com and deepflood.com at the same time and push hot posts
// @author       xykt
// @match        https://nodeseek.com/
// @match        https://www.nodeseek.com/
// @match        https://nodeseek.com/page-*
// @match        https://www.nodeseek.com/page-*
// @match        https://deepflood.com/
// @match        https://www.deepflood.com/
// @match        https://deepflood.com/page-*
// @match        https://www.deepflood.com/page-*
// @grant        GM_xmlhttpRequest
// @connect      nodeseek.com
// @connect      www.nodeseek.com
// @connect      deepflood.com
// @connect      www.deepflood.com
// @icon         https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict';
  const host = location.hostname.replace(/^www\./, '');
  const A = host;
  const B = A === 'nodeseek.com' ? 'deepflood.com' : 'nodeseek.com';
  const scheme = location.protocol;
  const baseA = scheme + '//' + A;
  const baseB = 'https://' + B;
  const pathMatch = location.pathname.match(/^\/(page-\d+)?\/?$/);
  if (!pathMatch) return;
  const currentPath = pathMatch[1] ? '/' + pathMatch[1] : '/';
  document.documentElement.style.visibility = 'hidden';
  function parseHTML(html) {
    const parser = new DOMParser();
    return parser.parseFromString(html, 'text/html');
  }
  function isRelative(url) {
    if (!url || typeof url !== 'string') return false;
    url = url.trim();
    const lower = url.toLowerCase();
    if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('//') ||
        lower.startsWith('mailto:') || lower.startsWith('tel:') || lower.startsWith('#') ||
        lower.startsWith('data:') ) {
      return false;
    }
    return true;
  }
  function absolutizeUrl(url, base) {
    if (!url) return url;
    if (!isRelative(url)) return url;
    if (url.startsWith('/')) return base + url;
    return base + '/' + url;
  }
  function convertRelativePaths(doc, base) {
    const attrList = ['href','src','action','poster','data-src','data-href'];
    attrList.forEach(attr => {
      doc.querySelectorAll('['+attr+']').forEach(el => {
        const val = el.getAttribute(attr);
        if (isRelative(val)) el.setAttribute(attr, absolutizeUrl(val, base));
      });
    });
    doc.querySelectorAll('[srcset]').forEach(el => {
      const ss = el.getAttribute('srcset') || '';
      const parts = ss.split(',').map(p => {
        const seg = p.trim();
        const spaceIdx = seg.indexOf(' ');
        if (spaceIdx === -1) {
          return isRelative(seg) ? absolutizeUrl(seg, base) : seg;
        } else {
          const u = seg.slice(0, spaceIdx);
          const rest = seg.slice(spaceIdx+1);
          return (isRelative(u) ? absolutizeUrl(u, base) : u) + ' ' + rest;
        }
      });
      el.setAttribute('srcset', parts.join(', '));
    });
    doc.querySelectorAll('[style]').forEach(el => {
      let s = el.getAttribute('style');
      if (!s) return;
      s = s.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
        (m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
      el.setAttribute('style', s);
    });
    doc.querySelectorAll('style').forEach(st => {
      let txt = st.textContent;
      if (!txt) return;
      txt = txt.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
        (m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
      st.textContent = txt;
    });
    return doc;
  }
  function fetchBAndMerge() {
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://' + B + currentPath,
      responseType: 'text',
      onload: function(res) {
        if (res.status >= 200 && res.status < 400 && res.responseText) {
          try {
            const docB = parseHTML(res.responseText);
            convertRelativePaths(docB, baseB);
            function doMerge() {
              try {
                const headA = document.querySelector('div#nsk-head.nsk-container, div#nsk-head');
                if (headA) {
                  const strong = headA.querySelector('strong.site-title');
                  let newInner = '';
                  if (A === 'nodeseek.com') {
                    newInner = `
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>`;
                  } else {
                    newInner = `
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>`;
                  }
                  if (strong) strong.innerHTML = newInner;
                }
                const containerA = document.querySelector('#nsk-left-panel-container');
                const panelsB = Array.from(docB.querySelectorAll('div.nsk-panel.category-list'));
                if (containerA && panelsB.length > 0) {
                    panelsB.forEach(bPanel => {
                        const cloned = document.importNode(bPanel, true);
                        containerA.appendChild(cloned);
                    });
                }
                (function handlePostList(docA, docB, site) {
                    const listA = Array.from(docA.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
                    const listB = Array.from(docB.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
                    const icons = {
                        nodeseek: `<span class="info-item info-site"><img src="https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span> `,
                        deepflood: `<span class="info-item info-site"><img src="https://www.deepflood.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span>`
                    };
                    const siteShort = (typeof site === 'string' && site.indexOf('nodeseek') !== -1) ? 'nodeseek' : 'deepflood';
                    const siteOther = siteShort === 'nodeseek' ? 'deepflood' : 'nodeseek';
                    function markSite(posts, siteFrom) {
                        posts.forEach(li => {
                            const info = li.querySelector(".post-info");
                            if (info) {
                                if (!info.querySelector('.info-item.info-site')) {
                                    info.insertAdjacentHTML("afterbegin", icons[siteFrom]);
                                }
                            }
                        });
                    }
                    markSite(listA, siteShort);
                    markSite(listB, siteOther);
                    let allPosts = [...listA, ...listB];
                    function parsePost(li) {
                        let timeTitle = "";
                        const timeEl = li.querySelector(".post-info time[title]");
                        if (timeEl) timeTitle = timeEl.getAttribute("title") || "";
                        let time = 0;
                        if (timeTitle) {
                            const parsed = Date.parse(timeTitle);
                            time = isNaN(parsed) ? (new Date(timeTitle)).getTime() || 0 : parsed;
                        }
                        let views = 0;
                        const viewsSpan = li.querySelector(".post-info .info-views span[title], .post-info .info-views span");
                        if (viewsSpan) {
                            const vt = viewsSpan.getAttribute("title") || viewsSpan.textContent || "";
                            const m = vt.match(/(\d[\d,]*)/);
                            if (m) views = parseInt(m[1].replace(/,/g, ''), 10) || 0;
                        }
                        let comments = 0;
                        const commentsSpan = li.querySelector(".post-info .info-comments-count span[title], .post-info .info-comments-count span");
                        if (commentsSpan) {
                            const ct = commentsSpan.getAttribute("title") || commentsSpan.textContent || "";
                            const m2 = ct.match(/(\d[\d,]*)/);
                            if (m2) comments = parseInt(m2[1].replace(/,/g, ''), 10) || 0;
                        }
                        const sticky = !!li.querySelector('use[href="#pin"], use[href="#pin"]');
                        return {
                            el: li,
                            time: time || 0,
                            weight: (views || 0) + (comments || 0) * 5,
                            sticky: !!sticky
                        };
                    }
                    let postsData = allPosts.map(parsePost);
                    let stickyPosts = postsData.filter(p => p.sticky).sort((a, b) => b.time - a.time);
                    let normalPosts = postsData.filter(p => !p.sticky);
                    normalPosts.sort((a, b) => b.weight - a.weight);
                    let hotPosts = normalPosts.slice(0, 5);
                    hotPosts.forEach(p => {
                        const info = p.el.querySelector(".post-info");
                        if (info) {
                            if (!info.querySelector('.info-item.info-hot')) {
                                info.insertAdjacentHTML("beforeend", `<span class="info-item info-hot"><svg class="iconpark-icon"><use href="#rocket" style="color: red;"></use></svg><a style="color: red;"> 热帖</a></span>`);
                            }
                        }
                    });
                    let otherPosts = normalPosts.slice(5).sort((a, b) => b.time - a.time);
                    let finalPosts = [...stickyPosts, ...hotPosts, ...otherPosts];
                    const postListA = docA.querySelector('ul.post-list:not([class*="topic-carousel"])');
                    if (postListA) {
                        postListA.innerHTML = "";
                        finalPosts.forEach(p => {
                            const nodeToInsert = (p.el.ownerDocument === document) ? p.el.cloneNode(true) : document.importNode(p.el, true);
                            postListA.appendChild(nodeToInsert);
                        });
                    }
                })(document, docB, A);
                const userCard = document.querySelector('div[data-v-244123cf].user-card, div.user-card[data-v-244123cf]');
                if (userCard) {
                  let newUserHtml = '';
                  if (A === 'nodeseek.com') {
                    newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>
<a href="https://deepflood.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>`;
                  } else {
                    newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>
<a href="https://nodeseek.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>`;
                  }
                  const nextDiv = userCard.nextElementSibling;
                  if (nextDiv && nextDiv.tagName.toLowerCase() === 'div') {
                    nextDiv.innerHTML = newUserHtml;
                  }
                }
                document.documentElement.style.visibility = '';
              } catch (err) {
                console.error('合并出错', err);
                document.documentElement.style.visibility = '';
              }
            }
            if (document.readyState === 'loading') {
              document.addEventListener('DOMContentLoaded', doMerge, {once:true});
            } else {
              doMerge();
            }
          } catch (e) {
            console.error('解析失败', e);
            document.documentElement.style.visibility = '';
          }
        } else {
          document.documentElement.style.visibility = '';
        }
      },
      onerror: function() {
        document.documentElement.style.visibility = '';
      }
    });
  }
  fetchBAndMerge();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址