Skip to content

Instantly share code, notes, and snippets.

@nmdra
Last active June 21, 2026 12:20
Show Gist options
  • Select an option

  • Save nmdra/6119e3d5ada9e55075f0f57361ca569e to your computer and use it in GitHub Desktop.

Select an option

Save nmdra/6119e3d5ada9e55075f0f57361ca569e to your computer and use it in GitHub Desktop.
medium.bypass.user.js
// ==UserScript==
// @name Medium Paywall Bypass
// @namespace Violentmonkey Scripts
// @run-at document-start
// @match *://*.medium.com/*
// @match *://medium.com/*
// @match *://*/*
// @grant none
// @version 4.1
// @inject-into content
// @updateURL https://gist.githubusercontent.com/mathix420/e0604ab0e916622972372711d2829555/raw/medium.user.js
// @downloadURL https://gist.githubusercontent.com/mathix420/e0604ab0e916622972372711d2829555/raw/medium.user.js
// @website freedium-mirror.cfd
// @author Mathix420, ZhymabekRoman, nmdra
// @description Bypass Medium paywall with availability check, fallback mirror, and manual bypass button.
// ==/UserScript==
(function () {
'use strict';
const MIRRORS = [
{ name: 'Freedium', base: 'https://freedium-mirror.cfd/', urlFn: (u) => 'https://freedium-mirror.cfd/' + u },
{ name: 'ReadMedium', base: 'https://readmedium.com/', urlFn: (u) => 'https://readmedium.com/' + u },
];
const CHECK_TIMEOUT = 10000;
// ── Helpers ──────────────────────────────────────────────────────────────
function isMediumArticle() {
return document.head
?.querySelector('meta[property="al:android:url"]')
?.content?.includes('medium://p/');
}
function isEditing() {
return window.location.href.includes('/edit?source=');
}
function isBypassed() {
return window.location.href.endsWith('#bypass');
}
function isAlreadyOnMirror() {
return MIRRORS.some(m => window.location.href.startsWith(m.base));
}
// ── Availability check ────────────────────────────────────────────────────
async function checkAvailable(url) {
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), CHECK_TIMEOUT);
await fetch(url, { method: 'HEAD', signal: ctrl.signal, mode: 'no-cors' });
clearTimeout(timer);
return true;
} catch {
return false;
}
}
// Check mirrors in order; return first available mirror object, or null
async function findAvailableMirror() {
for (const mirror of MIRRORS) {
const url = mirror.urlFn(window.location.href);
const ok = await checkAvailable(url);
if (ok) return { mirror, url };
}
return null;
}
// ── Toast UI ──────────────────────────────────────────────────────────────
const STYLES = `
#mpb-toast {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 2147483647;
font-family: -apple-system, 'Segoe UI', sans-serif;
font-size: 13px;
line-height: 1.4;
display: flex;
flex-direction: column;
gap: 0;
pointer-events: none;
}
#mpb-card {
background: #1a1a1a;
color: #f0f0f0;
border-radius: 12px;
padding: 14px 16px;
width: 296px;
box-shadow: 0 4px 24px rgba(0,0,0,0.35), 0 1px 4px rgba(0,0,0,0.2);
pointer-events: all;
transform: translateY(16px);
opacity: 0;
transition: opacity 200ms ease, transform 220ms ease;
}
#mpb-card.visible {
opacity: 1;
transform: translateY(0);
}
#mpb-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
#mpb-label {
display: flex;
align-items: center;
gap: 7px;
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #a0a0a0;
}
#mpb-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #a0a0a0;
flex-shrink: 0;
transition: background 300ms;
}
#mpb-dot.checking { background: #f5a623; animation: mpb-pulse 1s infinite; }
#mpb-dot.available { background: #3ecf8e; animation: none; }
#mpb-dot.fallback { background: #f5a623; animation: none; }
#mpb-dot.unavailable{ background: #e25a5a; animation: none; }
@keyframes mpb-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
#mpb-close {
background: none;
border: none;
color: #666;
cursor: pointer;
font-size: 17px;
line-height: 1;
padding: 0 2px;
transition: color 150ms;
}
#mpb-close:hover { color: #ccc; }
#mpb-status {
font-size: 13px;
color: #c8c8c8;
margin-bottom: 6px;
min-height: 18px;
}
#mpb-mirror-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
margin-bottom: 10px;
letter-spacing: 0.03em;
background: #252525;
color: #888;
border: 1px solid #2e2e2e;
transition: background 250ms, color 250ms, border-color 250ms;
}
#mpb-mirror-badge.primary { background: #0d2e1e; color: #3ecf8e; border-color: #1a4a30; }
#mpb-mirror-badge.fallback { background: #2e2200; color: #f5a623; border-color: #4a3600; }
#mpb-mirror-badge.unavail { background: #2e0d0d; color: #e25a5a; border-color: #4a1a1a; }
#mpb-mirror-dot {
width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex-shrink: 0;
}
#mpb-actions {
display: flex;
gap: 8px;
}
.mpb-btn {
flex: 1;
padding: 8px 10px;
border-radius: 8px;
border: none;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: opacity 150ms, transform 100ms;
letter-spacing: 0.02em;
}
.mpb-btn:active { transform: scale(0.97); }
.mpb-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
#mpb-btn-go {
background: #3ecf8e;
color: #0a2e1e;
}
#mpb-btn-go.fallback {
background: #f5a623;
color: #2e1a00;
}
#mpb-btn-go:hover:not(:disabled) { opacity: 0.88; }
#mpb-btn-skip {
background: #2a2a2a;
color: #a0a0a0;
border: 1px solid #3a3a3a;
}
#mpb-btn-skip:hover { background: #333; color: #ccc; }
#mpb-timer-bar {
height: 2px;
background: #2a2a2a;
border-radius: 0 0 12px 12px;
overflow: hidden;
margin-top: 0;
}
#mpb-timer-fill {
height: 100%;
background: #3ecf8e;
width: 100%;
transition: width linear;
}
#mpb-timer-fill.fallback { background: #f5a623; }
`;
function injectStyles() {
const s = document.createElement('style');
s.textContent = STYLES;
(document.head || document.documentElement).appendChild(s);
}
function buildToast() {
injectStyles();
const wrap = document.createElement('div');
wrap.id = 'mpb-toast';
wrap.innerHTML = `
<div id="mpb-card">
<div id="mpb-header">
<span id="mpb-label">
<span id="mpb-dot" class="checking"></span>
Paywall bypass
</span>
<button id="mpb-close" title="Dismiss">✕</button>
</div>
<div id="mpb-status">Checking availability…</div>
<div id="mpb-mirror-badge"><span id="mpb-mirror-dot"></span><span id="mpb-mirror-name">—</span></div>
<div id="mpb-actions">
<button class="mpb-btn" id="mpb-btn-go" disabled>Open free version</button>
<button class="mpb-btn" id="mpb-btn-skip">Stay here</button>
</div>
</div>
<div id="mpb-timer-bar"><div id="mpb-timer-fill"></div></div>
`;
document.documentElement.appendChild(wrap);
const card = wrap.querySelector('#mpb-card');
const dot = wrap.querySelector('#mpb-dot');
const status = wrap.querySelector('#mpb-status');
const badge = wrap.querySelector('#mpb-mirror-badge');
const badgeName = wrap.querySelector('#mpb-mirror-name');
const btnGo = wrap.querySelector('#mpb-btn-go');
const btnSkip = wrap.querySelector('#mpb-btn-skip');
const btnClose = wrap.querySelector('#mpb-close');
const timerFill = wrap.querySelector('#mpb-timer-fill');
let activeUrl = null;
let autoTick = null;
// animate in
requestAnimationFrame(() => {
requestAnimationFrame(() => card.classList.add('visible'));
});
function cancelAutoRedirect() {
if (autoTick) { clearInterval(autoTick); autoTick = null; }
}
function dismiss() {
cancelAutoRedirect();
card.style.transition = 'opacity 180ms ease, transform 180ms ease';
card.classList.remove('visible');
card.style.transform = 'translateY(8px)';
setTimeout(() => wrap.remove(), 200);
}
btnClose.addEventListener('click', dismiss);
btnSkip.addEventListener('click', dismiss);
btnGo.addEventListener('click', () => {
cancelAutoRedirect();
btnGo.textContent = 'Redirecting…';
btnGo.disabled = true;
window.location.href = activeUrl;
});
return {
setChecking(mirrorName) {
dot.className = 'checking';
status.textContent = mirrorName
? `Trying ${mirrorName}…`
: 'Checking availability…';
badge.className = '';
badgeName.textContent = mirrorName || '—';
btnGo.disabled = true;
btnGo.className = 'mpb-btn';
timerFill.className = '';
timerFill.style.transition = 'none';
timerFill.style.width = '100%';
},
setAvailable(mirror, url) {
activeUrl = url;
const isPrimary = mirror === MIRRORS[0];
dot.className = isPrimary ? 'available' : 'fallback';
badge.className = isPrimary ? 'primary' : 'fallback';
badgeName.textContent = mirror.name;
status.textContent = isPrimary
? 'Free version available — redirecting shortly.'
: `Primary mirror offline. Falling back to ${mirror.name}.`;
btnGo.disabled = false;
btnGo.textContent = 'Open free version';
btnGo.className = isPrimary ? 'mpb-btn' : 'mpb-btn fallback';
timerFill.className = isPrimary ? '' : 'fallback';
},
setUnavailable() {
activeUrl = MIRRORS[0].urlFn(window.location.href); // last-ditch manual
dot.className = 'unavailable';
badge.className = 'unavail';
badgeName.textContent = 'All mirrors offline';
status.textContent = 'All mirrors appear offline. Try manually.';
btnGo.disabled = false;
btnGo.textContent = 'Try anyway';
btnGo.className = 'mpb-btn';
timerFill.style.width = '0';
},
startAutoRedirect(seconds) {
timerFill.style.transition = `width ${seconds}s linear`;
requestAnimationFrame(() => { timerFill.style.width = '0%'; });
let remaining = seconds * 1000;
const interval = 50;
autoTick = setInterval(() => {
remaining -= interval;
if (remaining <= 0) {
cancelAutoRedirect();
if (!isBypassed()) window.location.href = activeUrl;
}
}, interval);
btnSkip.addEventListener('click', cancelAutoRedirect);
btnClose.addEventListener('click', cancelAutoRedirect);
btnGo.addEventListener('click', cancelAutoRedirect);
},
dismiss,
};
}
// ── Core redirecter ──────────────────────────────────────────────────────
let toastShown = false;
async function mediumRedirecter(initCall = false) {
if (
!isBypassed() &&
!isEditing() &&
!isAlreadyOnMirror() &&
!toastShown &&
isMediumArticle()
) {
toastShown = true;
const toast = buildToast();
// Check mirrors sequentially, updating the badge as we go
let found = null;
for (const mirror of MIRRORS) {
toast.setChecking(mirror.name);
const url = mirror.urlFn(window.location.href);
const ok = await checkAvailable(url);
if (ok) { found = { mirror, url }; break; }
}
if (found) {
toast.setAvailable(found.mirror, found.url);
toast.startAutoRedirect(4);
} else {
toast.setUnavailable();
}
} else if (initCall && /(.*\.|^)medium\.com$/.test(window.location.host)) {
new MutationObserver(function (mutations) {
if (mutations[0].target.textContent) mediumRedirecter();
}).observe(document.querySelector('title'), {
subtree: true,
characterData: true,
childList: true,
});
}
}
mediumRedirecter(true);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment