sehuatang

self mode

// ==UserScript==
// @name         sehuatang
// @version      0.0.12
// @author       bilabila
// @namespace    https://greasyfork.org/users/164996a
// @match        https://www.sehuatang.org/404
// @match        https://sehuatang.org/404
// @description  self mode
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      www.sehuatang.org
// @run-at       document-start
// ==/UserScript==
const tags = {
  fellatiojapan: ['亚洲无码原创', 'fellatiojapan'],
  handjobjapan: ['亚洲无码原创', 'handjobjapan'],
  uralesbian: ['亚洲无码原创', 'uralesbian'],
  spermmania: ['亚洲无码原创', 'spermmania'],
  legsjapan: ['亚洲无码原创', 'legsjapan'],
  无: ['亚洲无码原创', '全部'],
  有: ['亚洲有码原创', '全部'],
  素: ['素人有码系列', '全部'],
}
const head = `<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>sehuatang</title>
<style>
  :root {
    --color1: #444;
    --color2: #bbb;
  }
  body {
    color: var(--color1);
    text-align: center;
    margin: 0;
    box-sizing: border-box;
  }
  img {
    width: 100%;
  }
  ul {
    margin: 0;
    padding: 0;
    list-style-type: none;
  }
  li {
    color: var(--color1);
    margin: 0 6% 3em;
    padding: 0;
  }
  a {
    text-decoration: none;
  }
  .title {
    display: flex;
    justify-content: space-between;
    background-color: white;
  }
  #tag {
    margin: 1em 0;
  }
  #tag > span {
    display: inline-block;
    padding: 0.2em 0.8em;
    color: var(--color1);
    cursor: pointer;
    user-select: none;
  }
  #tag > span.disable {
    color: var(--color2);
  }
  #clear {
    position: absolute;
    top: 1em;
    right: 1em;
    padding: 0.2em;
    cursor: pointer;
    user-select: none;
    color: var(--color2);
  }
  #clear:hover {
    color: var(--color1)
  }
</style>`
const body = `<div id=app>
  <span id=clear>☢</span>
  <div id="tag"></div>
  <ul></ul>
</div>`
document.head.innerHTML = head
document.body.innerHTML = body
const ul = document.querySelector('ul')
Array.prototype.last = function (i = 1) {
  return this[this.length - i]
}
const gmFetch = (url, method = 'GET') =>
  new Promise((onload, onerror) => {
    GM_xmlhttpRequest({ url, method, onload, onerror, cookie: '_safe=' })
  })
const parseHTML = (str) => {
  const tmp = document.implementation.createHTMLDocument()
  tmp.body.innerHTML = str
  return tmp
}
const cache = async (k, f, ...args) => {
  if (k === undefined) return
  let a = GM_getValue(k)
  if (a) return JSON.parse(a)
  a = await f(...args)
  GM_setValue(k, JSON.stringify(a))
  return a
}
// get one post
const fetchOnePost = async (tid) => {
  const url = `https://www.sehuatang.org/forum.php?mod=viewthread&tid=${tid}`
  let a = await gmFetch(url)
  a = parseHTML(a.responseText)
  let title = a.querySelector('h1'),
    img = a.querySelectorAll('.pcb img'),
    magnet = a.querySelector('.blockcode li'),
    torrent = a.querySelector('.attnm > a')

  title = title ? title.textContent : ''
  const img_bl = [
    'https://cdn.jsdelivr.net/gh/hishis/forum-master/public/images/patch.gif',
  ]
  img = img
    ? [...img]
        .map((i) => i.getAttribute('file'))
        .filter((i) => i && !img_bl.includes(i))
    : []
  magnet = magnet ? magnet.textContent : ''
  torrent = torrent ? torrent.href : ''
  a = { img, magnet, torrent, title }
  return a
}
// get one page
const fetchOnePage = async (fid, typeid, page) => {
  let a = await gmFetch(
    `https://www.sehuatang.org/forum.php?mod=forumdisplay&fid=${fid}` +
      (typeid != '0' ? `&filter=typeid&typeid=${typeid}` : '') +
      `&page=${page}`
  )
  a = parseHTML(a.responseText)
  // check page
  // if (a.querySelector('#fd_page_top strong').textContent != page) return
  a = [
    ...a.querySelectorAll(
      '#threadlisttableid tbody[id^=normalthread_] th a[id^=content_]'
    ),
  ]
  return a.map((i) => parseInt(/content_(\d+)/.exec(i.id)[1]))
}
// get type and id of one fid
const fetchTypeId = async (fid) => {
  let a = await gmFetch(
    `https://www.sehuatang.org/forum.php?mod=forumdisplay&fid=${fid}`
  )
  a = parseHTML(a.responseText)
  a = a.querySelectorAll('#thread_types > li:not([id]) > a')
  a = [...a].filter((i) => i.firstChild)
  const b = { 全部: '0' }
  ;[...a].forEach(
    (i) => (b[i.firstChild.textContent] = /typeid=(\d+)/.exec(i.href)[1])
  )
  return b
}
// get all fid and name
const fetchFidName = async () => {
  const url = `https://www.sehuatang.org/forum.php`
  let a = await gmFetch(url)
  a = parseHTML(a.responseText)
  let ans = {}
  for (let b of a.querySelectorAll('a')) {
    if (b.childElementCount != 0) continue
    let fid = /(fid=|forum-)(\d+)/.exec(b.href)
    if (!fid) continue
    fid = fid[2]
    let name = b.textContent
    ans[name] = fid
  }
  return ans
}
class Bot {
  constructor(fid, typeid) {
    this.num_one_page = Number.MAX_SAFE_INTEGER
    this.key = fid + '_' + typeid
    this.data = JSON.parse(GM_getValue(this.key, '[]'))
    this.fid = fid
    this.typeid = typeid
    this.i1 = 0
  }
  async get(page) {
    return await fetchOnePage(this.fid, this.typeid, page)
  }
  async more(a) {
    if (a.page === -1) return
    const aa = a.arr,
      aal = aa.last(),
      b = await this.get(++a.page)
    if (!b) return (a.page = -1)
    let i = 0
    for (; i < b.length && b[i] >= aal; ++i) {}
    // end support for long time scroll interval
    //if (i === b.length) await this.more(a)
    if (i === b.length) return (a.page = -1)
    for (; i < b.length; ++i) aa.push(b[i])
  }
  merge() {
    const data = this.data
    if (data.length < 2) return
    const p = data.last(2),
      c = data.last(),
      pa = p.arr,
      ca = c.arr,
      cal = ca.last(),
      calen = ca.length
    if (cal > pa[0]) return
    let i = 1
    for (; i < pa.length && pa[i] >= cal; ++i) {}
    for (; i < pa.length; ++i) ca.push(pa[i])
    p.arr = ca
    if (p.page !== -1)
      p.page = (c.page + (ca.length - calen) / this.num_one_page) >> 0
    data.pop()
  }
  async refresh() {
    const data = this.data
    const p = data.last() && data.last().arr[0]
    data.push({
      arr: undefined,
      page: 0,
    })
    const a = data.last()
    a.arr = await this.get(++a.page)
    this.num_one_page = a.arr.length
    this.merge()
    if (p != data.last().arr[0]) {
      this.save()
      return true
    }
  }
  async next() {
    const data = this.data
    if (data.length === 0) return await this.refresh()
    await this.more(data.last())
    this.merge()
    this.save()
  }
  save() {
    GM_setValue(this.key, JSON.stringify(this.data))
  }
  async nextOne() {
    if (this.data.length === 0) await this.refresh()
    const a = this.data.last().arr
    if (this.i1 < a.length) return a[this.i1++]
    await this.next()
    return a[this.i1++]
  }
}
const li = (a) => {
  const { title, img, magnet, torrent } = a
  return `
    <li>
      ${img.map((i) => `<img src="${i}"/>`).join('')}
      <div class="title">
        <span>${title}</span>
        <span>
          <a href="${magnet}">magnet</a>
          <a href="${torrent}" target="_blank">torrent</a>
        </span>
      </div>
    </li>
  `
}
const addTag = () => {
  let ts = tags
  let t = GM_getValue('tag')
  t = t ? JSON.parse(t) : [Object.keys(ts)[0]]
  const n = document.querySelector('#tag')
  const a = Object.keys(ts)
    .map((i) => `<span ${t.includes(i) ? '' : 'class=disable'}>${i}</span>`)
    .join('')
  requestAnimationFrame(() => (n.innerHTML = a))
  n.addEventListener('click', (e) => {
    e = e.target
    if (e.nodeName != 'SPAN' || !e.classList.contains('disable')) return
    e.classList.add('disable')
    t = [e.textContent]
    GM_setValue('tag', JSON.stringify(t))
    window.location.reload()
  })
  return [t, ts]
}
const main = async () => {
  const [t, ts] = addTag()
  const v = []
  for (let x of t) {
    x = ts[t]
    let fid = await cache('fid', fetchFidName)
    fid = fid[x[0]]
    let typeid = await cache('typeid' + fid, fetchTypeId, fid)
    typeid = typeid[x[1]]
    v.push(new Bot(fid, typeid))
  }
  const q = await Promise.all(v.map(async (i) => await i.nextOne()))
  const next = async () => {
    let m = 0
    for (let i = 1; i < q.length; ++i)
      if (q[i] !== undefined && q[i] > q[m]) m = i
    if (q[m] === undefined) return
    const r = q[m]
    while (r === q[m]) {
      q[m] = await v[m].nextOne()
    }
    return r
  }
  let isEnd = false
  const add = async (a) => {
    if (a === undefined) {
      if (isEnd) return
      isEnd = true
      window.onscroll = null
      ul.insertAdjacentHTML(
        'afterend',
        `<span>total : ${ul.childElementCount}</span>`
      )
      const total = document.querySelector('ul+span')
      new MutationObserver(
        () => (total.innerHTML = `total : ${ul.childElementCount}`)
      ).observe(ul, { childList: true })
      return
    }
    a = await cache(a, fetchOnePost, a)
    if (!a) return
    requestAnimationFrame(() => ul.insertAdjacentHTML('beforeend', li(a)))
  }
  for (let i = 0; i < 3; ++i) await add(await next())
  const a = await Promise.all(v.map(async (i) => await i.refresh()))
  if (a.some((i) => i)) window.location.reload()
  let isFetching = false
  window.onscroll = async () => {
    if (
      !isFetching &&
      10 * window.innerHeight + window.scrollY >= document.body.offsetHeight
    ) {
      isFetching = true
      await add(await next())
      isFetching = false
    }
  }
}
const init = () => {
  for (let i of GM_listValues()) GM_deleteValue(i)
  GM_setValue('version', GM_info.script.version)
  window.location.reload()
}
window.onbeforeunload = () => window.scrollTo(0, 0)
if (GM_getValue('version') != GM_info.script.version) init()
document.querySelector('#clear').onclick = init
main()