Medium Member Bypass

Modern Medium GUI with multiple bypass services and fallback with availability checks, including custom domains and Freedium banner auto-close.

当前为 2025-01-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Medium Member Bypass
// @author       UniverseDev
// @license      GPL-3.0-or-later
// @namespace    http://tampermonkey.net/
// @version      13.9.2
// @description  Modern Medium GUI with multiple bypass services and fallback with availability checks, including custom domains and Freedium banner auto-close.
// @match        *://*.medium.com/*
// @match        *://*.betterprogramming.pub/*
// @match        *://*.towardsdatascience.com/*
// @match        https://freedium.cfd/*
// @match        https://readmedium.com/*
// @match        https://md.vern.cc/*
// @match        https://archive.is/*
// @match        https://archive.li/*
// @match        https://archive.vn/*
// @match        https://archive.ph/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      freedium.cfd
// @connect      readmedium.com
// @connect      md.vern.cc
// @connect      archive.is
// @connect      archive.li
// @connect      archive.vn
// @connect      archive.ph
// ==/UserScript==

(function() {
    'use strict';

    const SETTINGS_CLASS = 'medium-settings';
    const NOTIFICATION_CLASS = 'medium-notification';
    const MEMBER_DIV_SELECTOR = 'p.bf.b.bg.z.bk';
    const FREEDIUM_CLOSE_BUTTON_SELECTOR = '.close-button';
    const MEMBER_WALL_CHECK_SELECTOR = 'div.s.u.w.fg.fh.q';

    const getStoredValue = (key, defaultValue) => GM_getValue(key, defaultValue);
    const setStoredValue = (key, value) => GM_setValue(key, value);

    const MEDIUM_CUSTOM_DOMAINS = ['betterprogramming.pub', 'towardsdatascience.com'];

    const config = {
        bypassUrls: {
            freedium: 'https://freedium.cfd',
            readmedium: 'https://readmedium.com',
            libmedium: 'https://md.vern.cc/',
            archiveIs: 'https://archive.is/newest/',
            archiveLi: 'https://archive.li/newest/',
            archiveVn: 'https://archive.vn/newest/',
            archivePh: 'https://archive.ph/newest/',
        },
        currentBypassIndex: getStoredValue('currentBypassIndex', 0),
        memberOnlyDivSelector: MEMBER_DIV_SELECTOR,
        autoRedirectDelay: getStoredValue('redirectDelay', 5000),
        autoRedirectEnabled: getStoredValue('autoRedirect', true),
        darkModeEnabled: getStoredValue('darkModeEnabled', false),
        isBypassSession: getStoredValue('isBypassSession', false),
    };

    const isCurrentPageMediumDomain = () => window.location.hostname.endsWith('medium.com') || isCurrentPageMediumCustomDomain();
    const isCurrentPageMediumCustomDomain = () => MEDIUM_CUSTOM_DOMAINS.includes(window.location.hostname);

    let bypassServiceKeys = Object.keys(config.bypassUrls);
    let isCurrentlyRedirecting = false;

    const injectStyles = () => {
        const style = document.createElement('style');
        style.textContent = `
            .${SETTINGS_CLASS} {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 360px;
                background-color: var(--background-color, white);
                box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
                border-radius: 16px;
                font-family: 'Arial', sans-serif;
                z-index: 10000;
                padding: 20px;
                display: none;
                color: var(--text-color, #333);
                cursor: grab;
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-user-select: none;
            }
            .${SETTINGS_CLASS}.dark {
                --background-color: #333;
                --text-color: white;
            }
            .medium-settings-header {
                font-size: 22px;
                font-weight: bold;
                margin-bottom: 20px;
                text-align: center;
            }
            .medium-settings-toggle {
                margin: 15px 0;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .medium-settings-toggle > span {
                flex-grow: 1;
            }
            .medium-settings-input {
                margin-left: 10px;
                padding: 8px 10px;
                border: 1px solid #ccc;
                border-radius: 8px;
                box-sizing: border-box;
            }
            .medium-settings-input#redirectDelay {
                width: 70px;
            }
            .medium-settings-input#bypassSelector {
                width: 120px;
                appearance: auto;
                -webkit-appearance: auto;
                -moz-appearance: auto;
                background-repeat: no-repeat;
                background-position: right 10px center;
            }
            .${SETTINGS_CLASS}.dark .medium-settings-input#bypassSelector {
                border-color: #666;
            }
            .medium-settings-button {
                background-color: var(--button-bg-color, #1a8917);
                color: var(--button-text-color, white);
                border: none;
                padding: 8px 14px;
                border-radius: 20px;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.3s;
            }
            .medium-settings-button:hover {
                background-color: #155c11;
            }
            .${NOTIFICATION_CLASS} {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background-color: #1a8917;
                color: white;
                padding: 15px;
                border-radius: 20px;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                font-family: 'Arial', sans-serif;
                z-index: 10000;
                opacity: 0;
                transform: translateY(20px);
                transition: all 0.3s ease;
            }
            .${NOTIFICATION_CLASS}.show {
                opacity: 1;
                transform: translateY(0);
            }
            .medium-settings-input:focus {
                outline: none;
                border-color: #1a8917;
                box-shadow: 0 0 5px rgba(26, 137, 23, 0.3);
            }

            .switch {
              position: relative;
              display: inline-block;
              width: 40px;
              height: 24px;
            }

            .switch input {
              opacity: 0;
              width: 0;
              height: 0;
            }

            .slider {
              position: absolute;
              cursor: pointer;
              top: 0;
              left: 0;
              right: 0;
              bottom: 0;
              background-color: #ccc;
              transition: .4s;
            }

            .slider:before {
              position: absolute;
              content: "";
              height: 16px;
              width: 16px;
              left: 4px;
              bottom: 4px;
              background-color: white;
              transition: .4s;
            }

            input:checked + .slider {
              background-color: #1a8917;
            }

            input:focus + .slider {
              box-shadow: 0 0 1px #1a8917;
            }

            input:checked + .slider:before {
              transform: translateX(16px);
            }

            .slider.round {
              border-radius: 34px;
            }

            .slider.round:before {
              border-radius: 50%;
            }
        `;
        document.head.appendChild(style);
    };

    const showStealthNotification = (message) => {
        const notification = document.createElement('div');
        notification.className = NOTIFICATION_CLASS;
        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => notification.classList.add('show'), 50);
        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => notification.remove(), 300);
        }, 3000);
    };

    const getCurrentBypassServiceKey = () => {
        return bypassServiceKeys[config.currentBypassIndex % bypassServiceKeys.length];
    };

    const switchToNextBypassService = () => {
        config.currentBypassIndex++;
        setStoredValue('currentBypassIndex', config.currentBypassIndex);
        showStealthNotification(`Trying next bypass service: ${getCurrentBypassServiceKey()}`);
    };

    const checkServiceAvailability = async () => {
        const availabilityPromises = Object.entries(config.bypassUrls).map(async ([key, url]) => {
            try {
                const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' });
                return { key, available: response.ok || response.type === 'opaque' };
            } catch (error) {
                console.error(`Service unavailable: ${key} - ${url}`, error);
                return { key, available: false };
            }
        });
        const results = await Promise.allSettled(availabilityPromises);
        return results.reduce((accumulator, result) => {
            if (result.status === 'fulfilled') {
                accumulator[result.value.key] = result.value.available;
            }
            return accumulator;
        }, {});
    };

    const attemptNextBypass = async (articleUrl, attemptNumber) => {
        switchToNextBypassService();
        const nextBypassServiceKey = getCurrentBypassServiceKey();
        if (nextBypassServiceKey) {
            attemptBypass(articleUrl, nextBypassServiceKey, attemptNumber + 1);
        } else {
            console.error("No more bypass services to try.");
            showStealthNotification("All bypass attempts failed.");
        }
    };

    const attemptBypass = async (articleUrl, bypassKey, attemptNumber = 1) => {
        const bypassUrlValue = config.bypassUrls[bypassKey];
        const serviceAvailability = await checkServiceAvailability();

        if (!serviceAvailability[bypassKey]) {
            showStealthNotification(`Service unavailable: ${bypassKey}`);
            return attemptNextBypass(articleUrl, attemptNumber);
        }

        try {
            let bypassUrl;
            const mediumURL = new URL(decodeURIComponent(articleUrl));
            let articlePathname = mediumURL.pathname;

            if (bypassKey === 'libmedium') {
                if (articlePathname.startsWith('/')) {
                    articlePathname = articlePathname.substring(1);
                }
                bypassUrl = `${bypassUrlValue}${articlePathname}`;
            } else if (bypassKey.startsWith('archive')) {
                bypassUrl = bypassUrlValue + articleUrl + '#bypass';
            } else {
                const bypassBaseURL = new URL(bypassUrlValue);
                bypassUrl = new URL(mediumURL.pathname, bypassBaseURL).href;
            }
            isCurrentlyRedirecting = true;
            window.location.href = bypassUrl;
        } catch (error) {
            console.error(`Error during bypass with ${bypassKey}:`, error);
            showStealthNotification(`Bypass failed with ${bypassKey}.`);
            attemptNextBypass(articleUrl, attemptNumber);
        }
    };

    const attachSettingsPanelListeners = (settingsContainer) => {
        settingsContainer.querySelector('#bypassSelector').addEventListener('change', (event) => {
            const selectedKey = event.target.value;
            config.currentBypassIndex = bypassServiceKeys.indexOf(selectedKey);
            setStoredValue('currentBypassIndex', config.currentBypassIndex);
            showStealthNotification(`Bypass service set to ${selectedKey}`);
        });

        settingsContainer.querySelector('#toggleRedirectCheckbox').addEventListener('change', () => {
            config.autoRedirectEnabled = settingsContainer.querySelector('#toggleRedirectCheckbox').checked;
            setStoredValue('autoRedirect', config.autoRedirectEnabled);
            showStealthNotification('Auto-Redirect toggled');
        });

        settingsContainer.querySelector('#toggleDarkModeCheckbox').addEventListener('change', () => {
            config.darkModeEnabled = settingsContainer.querySelector('#toggleDarkModeCheckbox').checked;
            setStoredValue('darkModeEnabled', config.darkModeEnabled);
            settingsContainer.classList.toggle('dark', config.darkModeEnabled);
            showStealthNotification('Dark Mode toggled');
        });

        settingsContainer.querySelector('#bypassNow').addEventListener('click', async () => {
            showStealthNotification('Attempting bypass...');
            const currentArticleUrl = encodeURIComponent(window.location.href);
            const selectedBypassService = getCurrentBypassServiceKey();
            setStoredValue('isBypassSession', true);
            await attemptBypass(currentArticleUrl, selectedBypassService);
        });

        settingsContainer.querySelector('#resetDefaults').addEventListener('click', () => {
            config.autoRedirectDelay = 5000;
            config.autoRedirectEnabled = true;
            config.darkModeEnabled = false;
            config.currentBypassIndex = 0;

            setStoredValue('redirectDelay', config.autoRedirectDelay);
            setStoredValue('autoRedirect', config.autoRedirectEnabled);
            setStoredValue('darkModeEnabled', config.darkModeEnabled);
            setStoredValue('currentBypassIndex', config.currentBypassIndex);

            settingsContainer.querySelector('#redirectDelay').value = config.autoRedirectDelay;
            settingsContainer.querySelector('#toggleRedirectCheckbox').checked = config.autoRedirectEnabled;
            settingsContainer.querySelector('#toggleDarkModeCheckbox').checked = config.darkModeEnabled;
            settingsContainer.querySelector('#bypassSelector').innerHTML = bypassServiceKeys.map((key, index) => `
                <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
            `).join('');
            settingsContainer.classList.remove('dark');
            showStealthNotification('Settings reset to defaults');
        });

        settingsContainer.querySelector('#saveSettings').addEventListener('click', () => {
            const newDelay = parseInt(settingsContainer.querySelector('#redirectDelay').value, 10);
            if (!isNaN(newDelay) && newDelay >= 0) {
                config.autoRedirectDelay = newDelay;
                setStoredValue('redirectDelay', newDelay);
                showStealthNotification('Settings saved');
            }
        });

        settingsContainer.querySelector('#closeSettings').addEventListener('click', () => {
            settingsContainer.style.display = 'none';
        });

        settingsContainer.querySelectorAll('.medium-settings-input').forEach(input => {
            input.addEventListener('mousedown', (event) => {
                event.preventDefault();
            });
        });
    };

    const showMediumSettingsPanel = () => {
        let existingPanel = document.querySelector(`.${SETTINGS_CLASS}`);
        if (existingPanel) {
            existingPanel.style.display = 'block';
            return;
        }

        const settingsContainer = document.createElement('div');
        settingsContainer.className = `${SETTINGS_CLASS} ${config.darkModeEnabled ? 'dark' : ''}`;
        settingsContainer.innerHTML = `
            <div class="medium-settings-header">Medium Settings</div>
            <div class="medium-settings-toggle">
                <span>Auto-Redirect</span>
                <label class="switch">
                    <input type="checkbox" id="toggleRedirectCheckbox" ${config.autoRedirectEnabled ? 'checked' : ''}>
                    <span class="slider round"></span>
                </label>
            </div>
            <div class="medium-settings-toggle">
                <span>Redirect Delay (ms)</span>
                <input type="number" class="medium-settings-input" id="redirectDelay" value="${config.autoRedirectDelay}" />
            </div>
            <div class="medium-settings-toggle">
                <span>Dark Mode</span>
                <label class="switch">
                    <input type="checkbox" id="toggleDarkModeCheckbox" ${config.darkModeEnabled ? 'checked' : ''}>
                    <span class="slider round"></span>
                </label>
            </div>
            <div class="medium-settings-toggle">
                <span>Bypass Service</span>
                <select id="bypassSelector" class="medium-settings-input">
                    ${bypassServiceKeys.map((key, index) => `
                        <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
                    `).join('')}
                </select>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="bypassNow">Bypass Now</button>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="resetDefaults">Reset to Default</button>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="saveSettings">Save</button>
                <button class="medium-settings-button" id="closeSettings">Close</button>
            </div>
        `;

        attachSettingsPanelListeners(settingsContainer);

        let isDragging = false;
        let dragStartX, dragStartY;

        settingsContainer.addEventListener('mousedown', (event) => {
            isDragging = true;
            dragStartX = event.clientX - settingsContainer.offsetLeft;
            dragStartY = event.clientY - settingsContainer.offsetTop;
            settingsContainer.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (event) => {
            if (!isDragging) return;
            settingsContainer.style.left = `${event.clientX - dragStartX}px`;
            settingsContainer.style.top = `${event.clientY - dragStartY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            settingsContainer.style.cursor = 'grab';
        });

        document.body.appendChild(settingsContainer);
        settingsContainer.style.display = 'block';
    };

    const performAutoRedirect = async () => {
        isCurrentlyRedirecting = false;

        if (config.isBypassSession) {
            setStoredValue('isBypassSession', false);
            return;
        }

        if (config.autoRedirectEnabled && document.querySelector(MEMBER_WALL_CHECK_SELECTOR) && !isCurrentlyRedirecting) {
            const serviceAvailability = await checkServiceAvailability();
            let currentBypassKey = getCurrentBypassServiceKey();

            if (currentBypassKey && !serviceAvailability[currentBypassKey]) {
                showStealthNotification(`Current bypass service (${currentBypassKey}) is unavailable.`);
                switchToNextBypassService();
                const nextBypassKey = getCurrentBypassServiceKey();
                if (nextBypassKey) {
                    showStealthNotification(`Attempting bypass with ${nextBypassKey}...`);
                    setTimeout(async () => {
                        const currentArticleUrl = encodeURIComponent(window.location.href);
                        setStoredValue('isBypassSession', true);
                        await attemptBypass(currentArticleUrl, nextBypassKey);
                    }, config.autoRedirectDelay);
                } else {
                    showStealthNotification("No available bypass services to try.");
                }
                return;
            }

            if (currentBypassKey) {
                showStealthNotification(`Attempting bypass with ${currentBypassKey}...`);
                setTimeout(async () => {
                    const currentArticleUrl = encodeURIComponent(window.location.href);
                    setStoredValue('isBypassSession', true);
                    if (currentBypassKey.startsWith('archive')) {
                        removeBypassFragmentFromUrl();
                    }
                    await attemptBypass(currentArticleUrl, currentBypassKey);
                }, config.autoRedirectDelay);
            } else {
                showStealthNotification("No available bypass services to try.");
            }
        }
    };

    const removeBypassFragmentFromUrl = () => {
        const archiveDomains = ['archive.is', 'archive.li', 'archive.vn', 'archive.ph'];
        const currentDomain = window.location.hostname;

        if (archiveDomains.includes(currentDomain) && window.location.hash === '#bypass') {
            window.history.replaceState({}, document.title, window.location.pathname + window.location.search);
        }
    };

    const autoCloseFreediumBanner = () => {
        if (window.location.hostname === 'freedium.cfd') {
            window.addEventListener('load', () => {
                const closeButton = document.querySelector(FREEDIUM_CLOSE_BUTTON_SELECTOR);
                if (closeButton) {
                    closeButton.click();
                } else {
                    console.log('Freedium banner close button not found.');
                }
            });
        }
    };

    const initializeScript = () => {
        removeBypassFragmentFromUrl();
        injectStyles();
        autoCloseFreediumBanner();

        if (isCurrentPageMediumDomain()) {
            GM_registerMenuCommand('Open Medium Settings', showMediumSettingsPanel);
            performAutoRedirect();
        } else if (Object.values(config.bypassUrls).some((url) => window.location.href.startsWith(url) || bypassServiceKeys.some(key => key.startsWith('archive') && window.location.href.startsWith(config.bypassUrls[key])))) {
            isCurrentlyRedirecting = false;
        }
    };

    initializeScript();
})();