Last active
June 21, 2026 12:20
-
-
Save nmdra/6119e3d5ada9e55075f0f57361ca569e to your computer and use it in GitHub Desktop.
medium.bypass.user.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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