// ==UserScript==
// @name ThisVid.com Improved
// @license MIT
// @namespace http://tampermonkey.net/
// @version 4.0.3
// @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
// @match https://*.thisvid.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=thisvid.com
// @grant GM_addStyle
// @require https://unpkg.com/[email protected]/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 */
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); }
const { state } = new PersistentState({
filterDurationFrom: 0,
filterDurationTo: 600,
filterDuration: false,
filterPrivate: false,
filterPublic: false,
filterExcludeWords: "",
filterExclude: false,
filterIncludeWords: "",
filterInclude: false,
uiEnabled: true,
infiniteScrollEnabled: true
});
const stateLocale = reactive({
pagIndexLast: 1,
pagIndexCur: 1,
});
watch([() => state.filterDurationFrom, () => state.filterDurationTo], (a, b) => {
state.filterDurationFrom = parseIntegerOr(a[0], b[0]);
state.filterDurationTo = parseIntegerOr(a[1], b[1]);
if (state.filterDuration) filter_({ filterDuration: true });
});
watch(() => state.filterDuration, () => filter_({ filterDuration: true }));
watch(() => state.filterPrivate, () => filter_({ filterPrivate: true }));
watch(() => state.filterPublic, () => filter_({ filterPublic: true }));
watch(() => state.filterExclude, () => filter_({ filterExclude: true }));
watch(() => state.filterExcludeWords, () => {
if (state.filterExclude) filter_({ filterExclude: true });
}, { deep: true });
watch(() => state.filterInclude, () => filter_({ filterInclude: true }));
watch(() => state.filterIncludeWords, () => {
if (state.filterInclude) filter_({ filterInclude: true });
}, { deep: true });
//====================================================================================================
class THISVID_RULES {
constructor() {
const { href, pathname } = window.location;
this.PAGE_HAS_VIDEO = $('.tumbpu[title]') || document.querySelector('.thumbs-items .thumb-holder');
this.PAGINATION = $('.pagination');
this.PAGINATION_LAST = this.PAGINATION ? parseInt(
$('.pagination-next')?.previousElementSibling?.firstElementChild?.textContent ||
$('.pagination-list li:last-child')?.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 = (RULES.IS_WATCHLATER_KIND ? [RULES.CONTAINER] : document.querySelectorAll('.thumbs-items:not(.thumbs-members)'));
containers.forEach(c => {
const html = parseDOM(c.innerHTML);
c.innerHTML = "";
if (html) {
handleLoadedHTML(html, c);
}
});
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;
console.log(SponsaaLogo);
const { filter_, handleLoadedHTML } = new DataManager(RULES, state);
const router = new Router();