Question Extension to view onlyfans video PPV length?

GretaFM2

Lurker
Joined
Sep 16, 2024
Posts
10
Reaction score
0
I previously had an extension that allowed you to preview the length of videos that are a part of a bundle. I know it was either a developer extension or a tampermonkey script but I cant for the life of me remember what it was called. I found it on here, but I can't find it again. I'm sure someone will be able to tell me. Thanks in advance
 
I don't have much to add, however, last I heard, it's no longer working. And no way to make it work. That was a while ago, so I could be wrong. But I highly doubt there's any working method.
 
I had one that worked a couple of weeks ago but deleted it when I wiped my VM, completely forgot about it. I remember finding the name of it on either a discussion thread for a model or on one of these tool posts. Tried to retrace my steps with no luck
 
Not sure if you still need this, but there's this Chrome extension I use.
ofcheck.lol
 
I made this tampermonkey script to view the video duration of locked content.

Code:
// ==UserScript==
// @name OnlyFans Locked Video Durations
// @version 1.0.2
// @description Shows how long each video is inside a locked message bundle, before you pay for it.
// @match https://onlyfans.com/*
// @run-at document-start
// @grant none
// ==/UserScript==

// The durations are already sitting in the /api2/v2/chats/.../messages response,
// OF just never renders them. You can see it yourself: devtools > Network > filter
// "messages" > Preview > media > any video item. So all this does is grab that
// response on its way past and print the numbers onto the locked chip.
//
// 1.0.0 - first version
// 1.0.1 - fixed durations coming back in ms on some accounts
// 1.0.2 - dropped the total, per-video only

(function () {
'use strict';

var DEBUG = false; // flip this on if durations aren't showing up
var NUMBERED = false; // true gives you "1. 1:03" instead of just "1:03"

function log() {
if (!DEBUG) return;
console.log.apply(console, ['[OFDur]'].concat([].slice.call(arguments)));
}

// everything we've scraped so far, keyed by message id
var seen = new Map();

var API = /\/api2\/v2\/.*(messages|chats|posts|vault|stories)/i;

// ---- reading the response ----

function secs(v) {
if (typeof v === 'number' && isFinite(v) && v > 0) {
return v > 100000 ? Math.round(v / 1000) : Math.round(v); // ms on some endpoints
}
if (typeof v === 'string') {
if (/^\d+(\.\d+)?$/.test(v)) return secs(parseFloat(v));
var mm = v.match(/^(?:(\d+):)?(\d{1,2}):(\d{2})$/);
if (mm) return (+(mm[1] || 0)) * 3600 + (+mm[2]) * 60 + (+mm[3]);
}
return null;
}

// duration isn't always in the same spot (media.duration vs media.info.source.duration
// vs media.source.duration depending on the endpoint), so just dig for it
function durationOf(obj, depth) {
depth = depth || 0;
if (!obj || typeof obj !== 'object' || depth > 5) return null;

if (Array.isArray(obj)) {
for (var i = 0; i < obj.length; i++) {
var d = durationOf(obj[i], depth + 1);
if (d) return d;
}
return null;
}

var keys = ['duration', 'videoDuration', 'durationSeconds'];
for (var k = 0; k < keys.length; k++) {
if (keys[k] in obj) {
var s = secs(obj[keys[k]]);
if (s) return s;
}
}
for (var key in obj) {
if (obj[key] && typeof obj[key] === 'object') {
var found = durationOf(obj[key], depth + 1);
if (found) return found;
}
}
return null;
}

function stripTags(html) {
if (!html) return '';
try {
var doc = new DOMParser().parseFromString(String(html), 'text/html');
return doc.body ? doc.body.textContent || '' : '';
} catch (e) {
return String(html).replace(/<[^>]*>/g, ' ');
}
}

function clean(s) {
return String(s || '')
.replace(/[\u200B-\u200D\uFEFF\u00A0]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}

function money(v) {
if (v == null) return null;
var n = parseFloat(String(v).replace(/[^0-9.]/g, ''));
return isFinite(n) ? n : null;
}

function save(msg) {
var id = String(msg.id);
var vids = [];
var pics = 0;

for (var i = 0; i < msg.media.length; i++) {
var m = msg.media[i];
if (!m || typeof m !== 'object') continue;
var t = String(m.type || '').toLowerCase();
if (t === 'video' || t === 'gif') vids.push(durationOf(m) || 0);
else if (t === 'photo' || t === 'image') pics++;
}
if (!vids.length) return;

// a later response can be more complete than an earlier one, but don't downgrade
var old = seen.get(id);
if (old && old.vids.filter(Boolean).length >= vids.filter(Boolean).length) return;

seen.set(id, {
id: id,
vids: vids,
pics: pics,
price: money(msg.price),
text: clean(stripTags(msg.text))
});
log('got', id, vids.map(mmss).join(', '));
}

// the messages live at different depths depending on the endpoint, so walk the whole thing
function crawl(node, depth) {
depth = depth || 0;
if (!node || typeof node !== 'object' || depth > 8) return;
if (Array.isArray(node)) {
for (var i = 0; i < node.length; i++) crawl(node[i], depth + 1);
return;
}
if (node.id != null && Array.isArray(node.media) && node.media.length) save(node);
for (var k in node) {
if (node[k] && typeof node[k] === 'object') crawl(node[k], depth + 1);
}
}

function gotResponse(url, body) {
if (!body || !API.test(url)) return;
var data;
try { data = JSON.parse(body); } catch (e) { return; }
try { crawl(data); paintSoon(); } catch (e) { log('parse blew up', e); }
}

// ---- hooking the requests (needs @run-at document-start or the app beats us to it) ----

var realFetch = window.fetch;
if (realFetch) {
window.fetch = function () {
var args = arguments;
var p = realFetch.apply(this, args);
try {
return p.then(function (res) {
try {
var first = args[0];
var url = (typeof first === 'string' ? first : (first && first.url)) || res.url || '';
if (API.test(url)) {
res.clone().text().then(function (t) { gotResponse(url, t); }).catch(function () {});
}
} catch (e) {}
return res;
});
} catch (e) {
return p;
}
};
}

var realOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
try { this._ofdurUrl = String(url); } catch (e) {}
return realOpen.apply(this, arguments);
};

var realSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function () {
var xhr = this;
try {
xhr.addEventListener('load', function () {
try {
var url = xhr._ofdurUrl || '';
if (!API.test(url)) return;
if (xhr.responseType === '' || xhr.responseType === 'text') {
gotResponse(url, xhr.responseText);
} else if (xhr.responseType === 'json' && xhr.response) {
crawl(xhr.response);
paintSoon();
}
} catch (e) {}
});
} catch (e) {}
return realSend.apply(this, arguments);
};

// ---- matching a saved message to its bubble ----
// there is no message id anywhere in the chat DOM (checked - no id, no data-id,
// nothing on the wrapper), so we match on what the locked chip is showing:
// video count first, then the message text, then photo count and price.

function mmss(s) {
if (!s) return '?:??';
var h = Math.floor(s / 3600);
var m = Math.floor((s % 3600) / 60);
var sec = Math.round(s % 60);
var pad = function (n) { return n < 10 ? '0' + n : '' + n; };
return h ? h + ':' + pad(m) + ':' + pad(sec) : m + ':' + pad(sec);
}

function readChip(node) {
var info = { pics: null, vids: null, price: null, text: '' };

var list = node.querySelector('.b-purchase__list');
if (list) {
var items = list.querySelectorAll('.b-purchase__list-item');
for (var i = 0; i < items.length; i++) {
var icon = items[i].querySelector('svg[data-icon-name]');
var name = icon ? icon.getAttribute('data-icon-name') : '';
var countEl = items[i].querySelector('.b-purchase__list-item__count');
var n = countEl ? parseInt(String(countEl.textContent).replace(/[^0-9]/g, ''), 10) : NaN;
if (!isFinite(n)) continue;
if (name === 'icon-video') info.vids = n;
else if (name === 'icon-media' || name === 'icon-photo') info.pics = n;
}
}

var priceEl = node.querySelector('.m-price-lock .b-purchase__list-item__count, .b-post__unknown__price');
if (priceEl) info.price = money(priceEl.textContent);

var textEl = node.querySelector('.b-chat__message__text-holder');
if (textEl) info.text = clean(textEl.textContent);

return info;
}

function lookup(info) {
var pool = [];
seen.forEach(function (e) {
if (info.vids == null || e.vids.length === info.vids) pool.push(e);
});
if (!pool.length) return null;

// narrow, but only if the filter doesn't wipe everything out
function tighten(fn) {
var next = pool.filter(fn);
if (next.length) pool = next;
}
if (info.text) tighten(function (e) { return e.text === info.text; });
if (info.pics != null) tighten(function (e) { return e.pics === info.pics; });
if (info.price != null) tighten(function (e) { return e.price === info.price; });

return pool[0] || null;
}

// ---- drawing ----

function pill(text) {
var el = document.createElement('span');
el.textContent = text;
el.style.display = 'inline-block';
el.style.padding = '0 6px';
el.style.borderRadius = '9px';
el.style.fontSize = '11px';
el.style.lineHeight = '17px';
el.style.fontWeight = '500';
el.style.whiteSpace = 'nowrap';
el.style.border = '1px solid currentColor';
el.style.opacity = '.75'; // currentColor so it works in both themes
return el;
}

function buildRow(entry) {
var row = document.createElement('div');
row.setAttribute('data-ofdur', '1');
row.style.display = 'flex';
row.style.flexWrap = 'wrap';
row.style.gap = '4px';
row.style.margin = '6px 0 2px';
row.style.lineHeight = '17px';

for (var i = 0; i < entry.vids.length; i++) {
row.appendChild(pill(NUMBERED ? (i + 1) + '. ' + mmss(entry.vids[i]) : mmss(entry.vids[i])));
}
return row;
}

function attach(node, entry) {
if (node.querySelector('[data-ofdur]')) return;
var row = buildRow(entry);

// sits right under the photo/video counts and above the unlock button.
// can't go in the ul itself, that has g-text-ellipsis on it and long rows get cut off.
var icons = node.querySelector('.b-subscribe-block .content-icons');
if (icons && icons.parentNode) {
icons.parentNode.insertBefore(row, icons.nextSibling);
return;
}
var block = node.querySelector('.b-subscribe-block') || node.querySelector('.b-post__unknown');
if (block) {
block.appendChild(row);
return;
}
(node.querySelector('.b-chat__message__body') || node).appendChild(row);
}

function paint() {
if (!seen.size) return;
var bubbles = document.querySelectorAll('.b-chat__message, [at-attr="chat_message"]');
for (var i = 0; i < bubbles.length; i++) {
var node = bubbles[i];
if (node.querySelector('[data-ofdur]')) continue;
var info = readChip(node);
if (info.vids == null) continue; // not a locked bundle
var entry = lookup(info);
if (!entry) { log('nothing matches', info); continue; }
attach(node, entry);
}
}

// vue rips these out and re-renders them constantly, hence the observer + the interval
var pending = false;
function paintSoon() {
if (pending) return;
pending = true;
requestAnimationFrame(function () {
pending = false;
try { paint(); } catch (e) { log(e); }
});
}

function start() {
try {
new MutationObserver(paintSoon).observe(document.documentElement, {
childList: true,
subtree: true
});
} catch (e) {}
setInterval(paintSoon, 1500);
paintSoon();
}

if (document.documentElement) start();
else document.addEventListener('DOMContentLoaded', start, { once: true });

window.__ofdur = { seen: seen, paint: paint };
})();
 
Thank you for sharing. I noticed the video lengths don't appear on some messages.
 
Back
Top