thisvid infinite scroll and filter

[s]try to take over the world![/s] infinite scroll, filter: public, private, duration

As of 2024-01-26. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         thisvid infinite scroll and filter
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      2024-01-18
// @description  [s]try to take over the world![/s] infinite scroll, filter: public, private, duration
// @author       homo
// @match        https://thisvid.com/
// @match        https://thisvid.com/latest-updates/*
// @match        https://thisvid.com/tags/*
// @match        https://thisvid.com/categories/*
// @match        https://thisvid.com/*/?q=*
// @match        https://thisvid.com/members/*
// @exclude      https://thisvid.com/videos/*
// @icon         data:image/jpeg;base64,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
// @grant        GM_addStyle
// ==/UserScript==
(function () {
    'use strict';

    console.clear();
    console.log('\n\ntapermonkey-thisvidscript\n\n');

    class Utils {
        static $ = (x, e = document.body) => e.querySelector(x);
        static $$ = (x, e = document.body) => e.querySelectorAll(x);

        static findElementsBetweenIds = (startId, endId) => {
            const startElement = document.getElementById(startId);
            const endElement = document.getElementById(endId);

            if (!startElement || !endElement) {
                return [];
            }

            const elementsBetween = [];
            let currentElement = startElement.nextElementSibling;

            while (currentElement && currentElement !== endElement) {
                elementsBetween.push(currentElement);
                currentElement = currentElement.nextElementSibling;
            }

            return elementsBetween;
        };

        static parseHTML = (str) => {
            var temp = document.createElement('html');
            temp.innerHTML = str;
            return temp;
        };

        static fetchHtml = async (url) =>
        fetch(url)
        .then((r) => r.text())
        .then((d) => parseHTML(d));

        static getRandomRgb = () => {
            const n = Math.round(0xffffff * Math.random());
            return `rgb(${n >> 16},${(n >> 8) & 255},${n & 255})`;
        };

        static parseCSSUrl = (s) => s.match(/(?<=\(").*(?="\))/)[0];

        static timeToSeconds = (t) =>
        t
        .match(/\d+/gm)
        .reverse()
        .map((s, i) => parseInt(s) * Math.pow(60, i))
        .reduce((a, b) => a + b);

        static circularShift = (n, c = 6, s = 1) => (n + s) % c || c;

        static isElementInViewport = (el) => {
            var rect = el.getBoundingClientRect();
            return (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <=
                (window.innerHeight ||
                 document.documentElement
                 .clientHeight) /* or $(window).height() */ &&
                rect.right <=
                (window.innerWidth ||
                 document.documentElement.clientWidth) /* or $(window).width() */
            );
        };

        static toggleClass = (element, className, condition) => {
            if (condition) {
                element.classList.add(className);
            } else {
                element.classList.remove(className);
            }
        };

        static parseDom = (html) => {
            return new DOMParser().parseFromString(html, 'text/html').body.firstChild;
        }
    }

    const {
        $,
        $$,
        findElementsBetweenIds,
        fetchHtml,
        parseHTML,
        parseCSSUrl,
        circularShift,
        isElementInViewport,
        timeToSeconds,
        getRandomRgb,
        toggleClass,
        parseDom
    } = Utils;

    class Tick {
        constructor(interval) {
            this.interval = interval;
        }

        start(callback) {
            if (this.ticker) {
                this.stop();
            }
            callback();
            this.ticker = setInterval(callback, this.interval);
        }

        stop() {
            clearInterval(this.ticker);
            this.ticker = false;
        }
    }

    class QueueDuplicatesFilter {
        constructor(queueLimit = 120) {
            this.queueLimit = queueLimit;
            this.queue = [];
        }

        put(item) {
            const isUnique = !this.queue.includes(item);
            if (isUnique) {
                if (this.queue.length > this.queueLimit) {
                    this.queue.shift();
                }
                this.queue.push(item);
            }
            return isUnique;
        }
    }

    class ReactiveLocalStorage {
        constructor(data) {
            if (data) {
                Object.assign(this, data);
                this.observeProps(this);
            }
        }

        getLS(prop) {
            const val = localStorage.getItem(prop);
            if (val !== null) {
                return JSON.parse(val);
            } else {
                return null;
            }
        }

        setLS(prop, value) {
            localStorage.setItem(prop, JSON.stringify(value));
        }

        toObservable(obj, prop) {
            var value = this.getLS(prop) || obj[prop];

            Object.defineProperty(obj, prop, {
                get() {
                    return value;
                },
                set(newValue) {
                    this.setLS(prop, newValue);
                    value = newValue;
                },
            });
        }

        observeProps(obj) {
            for (const [key, _] of Object.entries(obj)) {
                this.toObservable(obj, key);
            }
        }
    }

    const SCROLL_RESET_DELAY = 500;
    const ANIMATION_DELAY = 750;

    class DomManager {
        constructor() {
            GM_addStyle(`
            .filtered-private, .filtered-duration, .filtered-public
            { display: none !important; } `);
        }

        static thumbIsPrivate(t) {
            return t.firstElementChild.classList.contains('private');
        }

        static filterPrivate(container = document.body) {
            const tumbs = $$('.tumbpu', container);
            for (const t of tumbs) {
                toggleClass(t, 'filtered-private',
                            state.filterPrivate && DomManager.thumbIsPrivate(t));
            }
        }

        static filterPublic(container = document.body) {
            const tumbs = $$('.tumbpu', container);
            for (const t of tumbs) {
                toggleClass(t, 'filtered-public',
                            state.filterPublic && !DomManager.thumbIsPrivate(t));
            }
        }

        static filterByDuration(container = document.body) {
            const elements = $$('.duration', container);
            const {
                filterDurationFrom: from,
                filterDurationTo: to,
                filterDuration,
            } = state;
            for (const t of elements) {
                const te = t.parentElement.parentElement;
                let flag = false;
                if (filterDuration) {
                    const ts = timeToSeconds(t.innerText);
                    flag = ts < from || ts > to;
                }
                toggleClass(te, 'filtered-duration', filterDuration && flag);
            }
        }

        static createThumbsContainer() {
            const c = document.createElement('div');
            c.className = 'thumbs-items';
            return c;
        }

        static handleLoadedHTML = (htmlPage, mount, useStateContainer = true) => {
            const thumbs = $$('.tumbpu', htmlPage);

            let container = !useStateContainer
            ? DomManager.createThumbsContainer()
            : (state.container =
               state.container || DomManager.createThumbsContainer());

            for (const thumbElement of thumbs) {
                if (!uniquesQueue.put(thumbElement.getAttribute('href'))) {
                    thumbElement.remove();
                } else {
                    const privateEl = $('.private', thumbElement);
                    const img = $('img', thumbElement);
                    if (privateEl) {
                        img.src = parseCSSUrl(privateEl.style.background);
                        privateEl.style.background = '#000';
                    } else {
                        img.src = img.getAttribute('data-original');
                    }
                    img.classList.add('tracking');
                    container.appendChild(thumbElement);
                }
            }

            if (state.filterPrivate) filterPrivate(container);
            if (state.filterPublic) filterPublic(container);
            if (state.filterDuration) filterByDuration(container);
            mount.before(container);
        };
    }

    class PreviewManager {
        constructor() {
            this.tick = new Tick(ANIMATION_DELAY);
        }

        iteratePreviewImages(src) {
            return src.replace(/(\d)\.jpg$/, (_, num) => circularShift(parseInt(num)) + '.jpg');
        }

        animatePreview = (e) => {
            const { target: el, type } = e;
            if (el.tagName === 'IMG' && el.classList.contains('tracking')) {
                if (type === 'mouseout') {
                    this.tick.stop();
                    if (el.getAttribute('orig')) el.src = el.getAttribute('orig');
                }
                if (type === 'mouseover') {
                    this.tick.start(() => {
                        if (!el.getAttribute('orig')) el.setAttribute('orig', el.src);
                        el.src = this.iteratePreviewImages(el.src);
                    });
                }
            }
        };

        listen(e) {
            e.addEventListener('mouseout', this.animatePreview);
            e.addEventListener('mouseover', this.animatePreview);
        }
    }

    class PaginationPageManager {
        constructor() {
            this.pagination = $('.pagination');
            handleLoadedHTML(document.body, this.pagination);
            previewManager.listen(this.pagination.parentElement);
            this.paginationGenerator = this.createNextPageGenerator();
            this.generatorDone = false;
            this.infiniteScrollTriggered = false;
            this.tick = new Tick(SCROLL_RESET_DELAY);
            this.fixScrollViewPort();
        }

        fixScrollViewPort(){
            this.tick.start(() => {
                if (this.generatorDone) this.tick.stop();
                if (isElementInViewport(this.pagination)) this.generatorConsumer();
            });
        }

        async generatorConsumer() {
            const { value, done } = this.paginationGenerator.next();
            this.generatorDone = done;
            if (!done) {
                const nextPageHTML = await fetchHtml(value);
                const prevScrollPos = document.documentElement.scrollTop;
                handleLoadedHTML(nextPageHTML, this.pagination);
                window.scrollTo(0, prevScrollPos);
            }
        }

        createNextPageGenerator() {
            const pl = $('.pagination-next').previousElementSibling.firstElementChild.textContent;
            const offsetLast = parseInt(pl);

            let {origin, pathname, search } = window.location;
            let offset;
            [pathname, offset='1'] = pathname.split(/(\d+\/)$/);
            offset = parseInt(offset);
            pathname = pathname === '/' ? '/latest-updates/' : pathname;

            function* nextPageGenerator() {
                for (let c = offset + 1; c <= offsetLast; c++) {
                    const url = `${origin}${pathname}${c}/${search}`;
                    console.log(url);
                    yield url;
                }
            }

            return nextPageGenerator();
        }

        resetScroller = () => {
            this.infiniteScrollTriggered = false;
            window.dispatchEvent(new CustomEvent('scroll'));
        };

        infiniteScroll = () => {
            const inViewport = isElementInViewport(this.pagination);
            if (inViewport === this.infiniteScrollTriggered) return;
            this.infiniteScrollTriggered = inViewport;
            if (inViewport) this.generatorConsumer();
        };
    }

    class Router {
        constructor() {
            this.route();
        }

        route() {
            const { pathname } = window.location;
            if ($('.pagination-next') instanceof HTMLElement) {
                this.handlePaginationPage();
            } else if (/\/members\/\d+\/$/.test(pathname)) {
                this.handleMemberPage();
            } else if (/\/tag\//.test(pathname) || /\/?q=.*/.test(pathname)) {
                this.handlePageWithVideosButNoPagination();
            }
        }

        handlePageWithVideosButNoPagination() {
            const vid = $('.tumbpu');
            if (!vid) return;
            handleLoadedHTML(document.body, vid.parentElement);
            previewManager.listen(vid.parentElement);
        }

        handlePaginationPage() {
            this.paginationManager = new PaginationPageManager();
        }

        handleMemberPage() {
            const privates = $('#list_videos_private_videos_items');
            if (privates) {
                const mistakes = findElementsBetweenIds(
                    'list_videos_private_videos_items',
                    'list_videos_favourite_videos'
                );
                mistakes.forEach((m) => privates.appendChild(m));
                handleLoadedHTML(privates, privates, false);
            }

            const favorites = $('#list_videos_favourite_videos');
            if (favorites) {
                const mountTo = favorites.firstElementChild.nextElementSibling;
                handleLoadedHTML(favorites, mountTo, false);
            }

            if (privates || favorites) {
                previewManager.listen((privates || favorites).parentElement);
            }
        }
    }

    class UI {
        templateHTML = `
<div id="tapermonkey-app">
  <div class="subbox">
    <input type="checkbox" id="filterPrivate" name="filterPrivate" ${
      state.filterPrivate ? 'checked' : ''
    }/>
    <label for="filterPrivate">filter private</label>
    <input type="checkbox" id="filterPublic" name="filterPublic" ${
      state.filterPublic ? 'checked' : ''
    }/>
    <label for="filterPublic">filter public</label>
  </div>
  <div class="subbox">
    <input type="checkbox" id="filterl" name="filterl" ${
      state.filterDuration ? 'checked' : ''
    } />
    <label for="filterl">filter duration seconds</label>

    <input type="number" placeholder="min sec" step="10"
      min="0" max="100000" id="minL" value=${state.filterDurationFrom} />
    <input type="number" placeholder="max sec" step="10"
      min="0" max="100000" id="maxL" value=${state.filterDurationTo} />
  </div>
</div>`;

        templateCSS = `
    #tapermonkey-app {
      background: #151515;
      color: #fff;
      padding: 10px;
      position: fixed;
      z-index: 9999;
      bottom: 10px;
      right: 10px;
      border-radius: 15px;
      width: max-content;
      box-shadow: 20px 20px 60px #000000, -20px -20px 60px #000000;
    }
    #tapermonkey-app .subbox {
      background: #2c2c2c;
      border-radius: 5px;
      padding: 4px;
      margin: 6px;
    }
    #tapermonkey-app .subbox input[type=number] {
      padding-left: 10px;
      width: 5rem;
      background: #26282b;
    }
    #tapermonkey-app .subbox input[type=checkbox] {
      margin-left: 5px;
    }
    #tapermonkey-app .subbox label {
      user-select: none;
    }
    #tapermonkey-app .subbox input,
    #tapermonkey-app .subbox label {
      padding-left: 8px;
      float: none;
      width: auto;
      font-family: monospace;
      font-size: 0.8rem;
    }`;

        constructor() {
            document.body.appendChild(parseDom(this.templateHTML));
            this.tapermonkeyAppTemplate = document.querySelector('#tapermonkey-app');
            GM_addStyle(this.templateCSS);
            this.control();
        }

        control() {
            this.tapermonkeyAppTemplate.addEventListener('click', (e) => {
                if (e.target.id === 'filterPublic') {
                    state.filterPublic = e.target.checked;
                    filterPublic();
                }
                if (e.target.id === 'filterPrivate') {
                    state.filterPrivate = e.target.checked;
                    filterPrivate();
                }
                if (e.target.id === 'filterl') {
                    state.filterDuration = e.target.checked;
                    filterByDuration();
                }
                if (e.target.id === 'minL') {
                    state.filterDurationFrom =
                        parseInt(e.target.value) || state.filterDurationFrom;
                    filterByDuration();
                }
                if (e.target.id === 'maxL') {
                    state.filterDurationTo =
                        parseInt(e.target.value) || state.filterDurationTo;
                    filterByDuration();
                }
            });
        }
    }

    const state = new ReactiveLocalStorage({
        filterDurationFrom: 600,
        filterDurationTo: 100000,
        filterDuration: false,
        filterPrivate: false,
        filterPublic: false,
        infiniteScrollTriggered: false,
    });
    const uniquesQueue = new QueueDuplicatesFilter();
    const domManager = new DomManager();
    const { filterPrivate, filterPublic, filterByDuration, handleLoadedHTML } = DomManager;
    const previewManager = new PreviewManager();
    const router = new Router();
    const ui = new UI();
})();