// ==UserScript==
// @name ThisVid.com Improved
// @license MIT
// @namespace http://tampermonkey.net/
// @version 4.2
// @description Infinite scroll (optional). Lazy loading. Preview for private videos. Filter: duration, public/private, include/exclude terms. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼
// @author smartacephale
// @supportURL https://github.com/smartacephale/sleazy-fork
// @match https://*.thisvid.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant GM_addStyle
// @require https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js
// @require https://update.greasyfork.org/scripts/494206/utils.user.js
// @require data:, let tempVue = unsafeWindow.Vue; unsafeWindow.Vue = Vue; const { ref, watch, reactive, createApp } = Vue;
// @require https://update.greasyfork.org/scripts/494207/persistent-state.user.js
// @require https://update.greasyfork.org/scripts/494204/data-manager.user.js
// @require https://update.greasyfork.org/scripts/494205/pagination-manager.user.js
// @require https://update.greasyfork.org/scripts/494203/vue-ui.user.js
// @run-at document-idle
// ==/UserScript==
/* globals jQuery, $, Vue, createApp, watch, reactive, rxjs, listenEvents, range, Tick, waitForElementExists,
timeToSeconds, parseDOM, parseIntegerOr, fetchHtml, stringToWords, parseCSSUrl, circularShift, fetchText, Observer
LazyImgLoader, PersistentState, DataManager, PaginationManager, VueUI, DefaultState */
const SponsaaLogo = `
Kono bangumi ha(wa) goran no suponsaa no teikyou de okurishimasu⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⣟⢻⢛⢟⠿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣾⣾⣵⣧⣷⢽⢮⢧⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣯⣭⣧⣯⣮⣧⣯⣧⣯⡮⣵⣱⢕⣕⢕⣕⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⡫⡻⣝⢯⡻⣝⡟⣟⢽⡫⡟⣏⢏⡏⡝⡭⡹⡩⣻⣿⣿⣿⣿⠟⠟⢟⡟⠟⠻⠛⠟⠻⠻⣿⣿⣿⡟⠟⠻⠛⠟⠻⠻⣿⣿
⣿⣿⣿⣿⡿⣻⣿⣿⣿⡿⣿⣿⡿⣿⣿⢿⢿⡻⢾⠽⡺⡞⣗⠷⣿⣿⣿⡏⠀⠀⠀⣣⣤⡄⠀⠠⣄⡆⠫⠋⠻⢕⣤⡄⠀ ⢀⣤⣔⣿⣿
⣿⣿⣿⣿⣷⣷⣷⣾⣶⣯⣶⣶⣷⣷⣾⣷⣳⣵⣧⣳⡵⣕⣮⣞⣾⣿⡟⠄⢀⣦⠀⢘⣽⡇⠀⠨⣿⡌⠀⠣⣠⠹⠿⡭⠀ ⠐⣿⣿⣿⣿
⣿⣿⣿⣿⣕⣵⣱⣫⣳⡯⣯⣫⣯⣞⣮⣎⣮⣪⣢⣣⣝⣜⡜⣜⣾⣿⠃⠀⠀⠑⠀⠀⢺⡇⠀ ⢘⣾⠀⢄⢄⠘⠀⢘⢎⠀⢈⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣙⣛⣛⢻⢛⢟⢟⣛⢻⢹⣙⢳⢹⢚⢕⣓⡓⡏⣗⣿⣓⣀⣀⣿⣿⣮⢀⣀⣇⣀⣐⣿⣔⣀⢁⢀⣀⣀⣅⣀⡠⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣾⡞⣞⢷⡻⡯⡷⣗⢯⢷⢞⢷⢻⢞⢷⡳⣻⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣷⣵⡵⣼⢼⢼⡴⣵⢵⡵⣵⢵⡵⣵⣪⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⣧⣫⣪⡪⡣⣫⣪⣣⣯⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿`;
function $(x, e = document.body) { return e.querySelector(x); }
function $$(x, e = document.body) { return e.querySelectorAll(x); }
class THISVID_RULES {
constructor() {
const { href, pathname } = window.location;
this.PAGE_HAS_VIDEO = document.querySelector('.tumbpu[title], .thumbs-items .thumb-holder');
this.PAGINATION = $('.pagination');
this.PAGINATION_LAST = this.PAGINATION ? parseInt($('.pagination-next')?.previousElementSibling?.innerText) : 1;
this.CONTAINER = Array.from(document.querySelectorAll('.thumbs-items')).pop();
this.IS_MEMBER_PAGE = /\/members\/\d+\/$/.test(pathname);
this.IS_OTHER_MEMBER_PAGE = !$('.my-avatar') && this.IS_MEMBER_PAGE;
// highlight friend page profile
this.IS_MEMBER_FRIEND = this.IS_OTHER_MEMBER_PAGE && document.querySelector('.case-left')?.innerText.includes('is in your friends');
if (this.IS_MEMBER_FRIEND) {
document.querySelector('.profile').style.background = 'radial-gradient(circle, rgb(28, 42, 50) 48%, rgb(0, 0, 0) 100%)';
}
this.IS_WATCHLATER_KIND = /^\/my_(\w+)_videos\//.test(pathname);
this.IS_MESSAGES_PAGE = /\/my_messages\//.test(pathname);
this.IS_PLAYLIST = /^\/playlist\/\d+\//.test(pathname);
// playlist page add link to video
if (this.IS_PLAYLIST) {
const videoUrl = this.PLAYLIST_THUMB_URL(window.location.pathname);
const desc = document.querySelector('.tools-left > li:nth-child(4) > .title-description');
const ahref = document.createElement('a');
ahref.href = videoUrl;
const container = desc.parentElement;
ahref.appendChild(desc);
container.appendChild(ahref);
}
}
GET_THUMBS(html) {
if (this.IS_WATCHLATER_KIND) {
return Array.from(html.querySelectorAll('.thumb-holder'));
}
let thumbs = Array.from($$('.tumbpu[title]', html));
if (thumbs.length === 0 && html.classList.contains('tumbpu')) thumbs = [html];
return thumbs.filter(thumb => !thumb.parentElement.classList.contains('thumbs-photo'));
}
PLAYLIST_THUMB_URL(src) {
return src.replace(/playlist\/\d+\/video/, () => 'videos');
}
THUMB_URL(thumb) {
if (this.IS_WATCHLATER_KIND) {
return thumb.firstElementChild.href;
}
let url = thumb.getAttribute('href');
if (this.IS_PLAYLIST) url = this.PLAYLIST_THUMB_URL(url);
return url;
}
URL_DATA() {
const { origin, pathname, search } = window.location;
let offset = parseInt(pathname.split(/(\d+\/)$/)[1] || '1');
let pathname_ = pathname.split(/(\d+\/)$/)[0];
if (pathname === '/') pathname_ = '/latest-updates/';
let iteratable_url = (n) => `${origin}${pathname_}${n}/${search}`;
if (/^\/playlist\/\d+\/video\//.test(pathname)) {
offset = 1;
iteratable_url = n => `${origin}${pathname}?mode=async&function=get_block&block_id=playlist_view_playlist_view&sort_by=added2fav_date&from=${n}&_=${Date.now()}`;
}
return {
offset,
iteratable_url
}
}
THUMB_IMG_DATA(thumb) {
const img = $('img', thumb);
const privateThumb = $('.private', thumb);
let imgSrc = img?.getAttribute('data-original');
if (privateThumb) {
imgSrc = parseCSSUrl(privateThumb.style.background);
privateThumb.removeAttribute('style');
}
img.removeAttribute('data-cnt');
img.classList.remove('lazy-load');
img.classList.add('tracking');
if (this.IS_PLAYLIST) {
img.onmouseover = img.onmouseout = null;
img.removeAttribute('onmouseover');
img.removeAttribute('onmouseout');
}
return { img, imgSrc };
}
ITERATE_PREVIEW_IMG(src) {
return src.replace(/(\d)(?=\.jpg$)/, (_, n) => `${circularShift(parseInt(n))}`);
}
THUMB_DATA(thumb) {
const title = thumb.querySelector('.title').innerText.toLowerCase();
const duration = timeToSeconds(thumb.querySelector('.thumb > .duration').textContent);
return {
title,
duration
}
}
IS_PRIVATE(thumbElement) {
return thumbElement.firstElementChild.classList.contains('private');
}
PRIVATE_PREVIEW_FIX() {
unsafeWindow.$('img[alt!="Private"]').off();
}
}
const RULES = new THISVID_RULES();
//====================================================================================================
function friend(id, i = 0) {
return fetchText(FRIEND_REQUEST_URL(id))
.then((text) => console.log(`friend request #${i} with /members/662717/${id}/`, text));
}
const FRIEND_REQUEST_URL = (id) => `${window.location.origin}/members/${id}/?action=add_to_friends_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async&message=`;
function initFriendship() {
if (!RULES.IS_OTHER_MEMBER_PAGE) return;
createFriendButton();
function getUsers(el) {
const friendsList = el.querySelector('#list_members_friends_items');
if (!friendsList) return [];
return Array.from(friendsList.querySelectorAll('.tumbpu'))
.map(e => e.href.match(/\d+/)?.[0]).filter(_ => _);
}
const USERS_PER_PAGE = 24;
async function friendMemberFriends() {
const memberId = window.location.pathname.match(/\d+/)[0];
friend(memberId);
let friendsEl = $('#list_members_friends');
if (!friendsEl) return;
friendsEl = friendsEl.firstElementChild.innerText.match(/\d+/g);
const friendsCount = parseInt(friendsEl[friendsEl.length - 1]);
let friends;
if (friendsCount > 12) {
const offset = Math.ceil(friendsCount / USERS_PER_PAGE);
const pages = range(offset).map(o => `${window.location.origin}/members/${memberId}/friends/${o}/`);
const pagesFetched = pages.map(p => fetchHtml(p));
friends = (await Promise.all(pagesFetched)).flatMap(getUsers);
} else {
friends = getUsers(document.body);
}
await Promise.all(friends.map((fid, i) => friend(fid, i)));
}
function createFriendButton() {
GM_addStyle('.buttons {display: flex; flex-wrap: wrap} .buttons * {align-self: center; padding: 3px; margin: 1px;}');
const button = parseDOM('<button style="background: radial-gradient(red, blueviolet);">friend everyone</button>');
$('.buttons').appendChild(button);
button.addEventListener('click', () => {
button.style.background = 'radial-gradient(#ff6114, #5babc4)';
button.innerText = 'processing requests';
friendMemberFriends().then(() => {
button.style.background = 'radial-gradient(blue, lightgreen)';
button.innerText = 'friend requests sent';
});
}, { once: true });
}
}
//====================================================================================================
async function getUserData(id) {
const url = id.includes('member') ? id : `/members/${id}/`;
return fetchHtml(url).then(html => {
const memberVideos = unsafeWindow.$(html).find("span:contains('Videos uploaded')").children();
const privateVideosCount = parseInt(memberVideos[1].innerText);
const publicVideosCount = parseInt(memberVideos[0].innerText);
return {
publicVideosCount,
privateVideosCount
};
});
}
//====================================================================================================
function requestPrivateAccess(e, memberid) {
e.preventDefault();
friend(memberid);
e.target.innerText = e.target.innerText.replace('🚑', '🍆');
}
unsafeWindow.requestPrivateAccess = requestPrivateAccess;
const haveAccessColor = 'linear-gradient(90deg, #31623b, #212144)';
const haveNoAccessColor = 'linear-gradient(90deg, #462525, #46464a)';
function checkPrivateVidsAccess() {
document.querySelectorAll('.tumbpu > .private').forEach(t => {
const thumb = t.parentElement;
const url = thumb.href;
fetchHtml(url).then(html => {
const holder = html.querySelector('.video-holder > p');
const haveAccess = !holder;
thumb.style.background = haveAccess ? haveAccessColor : haveNoAccessColor;
thumb.querySelector('.title').innerText += haveAccess ? '✅' : '❌';
const uploaderEl = holder ? holder.querySelector('a') : html.querySelector('a.author');
const uploader = uploaderEl.href.replace(/.*\/(\d+)\/$/, (a, b) => b);
thumb.querySelector('.title').appendChild(parseDOM(holder ?
`<span onclick="requestPrivateAccess(event, ${uploader});"> 🚑 ${uploaderEl.innerText}</span>` :
`<span> 💅🏿 ${uploaderEl.innerText}</span>`));
});
});
}
//====================================================================================================
function download_(url, filename) {
fetch(url).then(t => t.blob()).then(b => {
const a = document.createElement("a");
a.href = URL.createObjectURL(b);
a.setAttribute("download", filename);
a.click();
});
}
function downloader() {
if (!location.pathname.startsWith('/videos/')) return;
function helper() {
unsafeWindow.$('.fp-ui').click();
waitForElementExists(document.body, 'video', (video) => {
const url = video.getAttribute('src');
const name = document.querySelector('.headline').innerText + '.mp4';
download_(url, name);
});
}
const btn = unsafeWindow.$('<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>');
unsafeWindow.$('.share_btn').after(btn);
btn.on('click', helper);
}
unsafeWindow.$(document).ready(downloader);
//====================================================================================================
class PreviewAnimation {
constructor(element, delay = ANIMATION_DELAY) {
this.tick = new Tick(delay);
listenEvents(element, ['mouseout', 'mouseover', 'touchstart', 'touchend'], this.animatePreview);
}
animatePreview = (e) => {
const { target: el, type } = e;
if (!el.classList.contains('tracking') || !el.getAttribute("src")) return;
this.tick.stop();
if (type === 'mouseover' || type === 'touchstart') {
const orig = el.getAttribute("src");
this.tick.start(
() => { el.src = RULES.ITERATE_PREVIEW_IMG(el.src); },
() => { el.src = orig; });
}
};
}
//====================================================================================================
class Router {
constructor() {
this.route();
}
route() {
const { href } = window.location;
const allowed_pagination = [
/\.com\/$/,
/\/(categories|tags?)\//,
/\/?q=.*/,
/\/(\w+-)?(rated|popular|private|newest|winners|updates)\/(\d+\/)?$/,
/\/members\/\d+\/\w+_videos\//,
/\/playlist\/\d+\//,
/\/my_(\w+)_videos\//
];
const isPaginationPage = allowed_pagination.some(r => r.test(href));
this.handleMessages();
if (!RULES.PAGE_HAS_VIDEO) return;
const containers = Array.from(RULES.IS_WATCHLATER_KIND ? [RULES.CONTAINER] : document.querySelectorAll('.thumbs-items:not(.thumbs-members)'));
if (containers.length > 1 && !RULES.IS_MEMBER_PAGE) RULES.CONTAINER = containers[0];
containers.forEach(c => {
handleLoadedHTML(c, RULES.IS_MEMBER_PAGE ? c : RULES.CONTAINER);
});
RULES.PRIVATE_PREVIEW_FIX();
new PreviewAnimation(document.body);
this.ui = new VueUI(state, stateLocale, true, checkPrivateVidsAccess);
this.handleMemberPage();
if (isPaginationPage) this.handlePaginationPage();
}
handlePaginationPage() {
stateLocale.pagIndexLast = RULES.PAGINATION_LAST;
if (!RULES.PAGINATION) return;
this.paginationManager = new PaginationManager(state, stateLocale, RULES, handleLoadedHTML, SCROLL_RESET_DELAY);
}
handleMemberPage() {
if (!RULES.IS_MEMBER_PAGE) return;
initFriendship();
}
handleMessages() {
if (!RULES.IS_MESSAGES_PAGE) return;
for (const member of $$('.user-avatar > a')) {
getUserData(member.href).then(({ publicVideosCount, privateVideosCount }) => {
if (privateVideosCount > 0) {
const succColor = 'linear-gradient(#2f6eb34f, #66666647)';
const failColor = 'linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28))';
const success = !member.parentElement.nextElementSibling.innerText.includes('declined');
member.parentElement.parentElement.style.background = success ? succColor : failColor;
}
$('.user-comment p', member.parentElement.parentElement).innerText += ` | videos: ${publicVideosCount} public, ${privateVideosCount} private`;
})
}
}
}
//====================================================================================================
const SCROLL_RESET_DELAY = 350;
const ANIMATION_DELAY = 750;
const defaultState = new DefaultState(true);
const { state, stateLocale } = defaultState;
const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
defaultState.setWatchers(filter_);
const router = new Router();