Onlyfans downloader

Help you to download onlyfans' media

As of 12.03.2022. See ბოლო ვერსია.

// ==UserScript==
// @name         Onlyfans downloader
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Help you to download onlyfans' media
// @author       Unknownman
// @match        https://onlyfans.com/*
// @icon         https://static.onlyfans.com/theme/onlyfans/spa/icons/favicon-notification-32x32.png
// @license      GNU GPLv3
// @grant        GM_download
// ==/UserScript==
(function (){
  const parentId = createRandomStr();
  const statusId = createRandomStr();
  const scrollToEndCheckboxId = createRandomStr();
  const videoIncludeCheckboxId = createRandomStr();
  const imageIncludeCheckboxId = createRandomStr();
  const downloadButtonId = createRandomStr();
  const closeButtonId = createRandomStr();
  const forceStartDownloadButtonId = createRandomStr();
  const listWrapperSelector = '#content > div.l-wrapper > div.l-wrapper__content > div.b-feed-content.g-sides-gaps > div.b-photos.g-negative-sides-gaps';
  const scrollToEndMaxRetryTimes = 20;
  const sleepMs = 500;
  let isForceStartToDownload = false;

  let hasSetProxy = false;
  let data = {
    videos: 0,
    images: 0,
    collectedVideos: 0,
    collectedImages: 0,
    downloadedVideos: 0,
    downloadedImages: 0,
  };
  const dataIds = {
    videos: createRandomStr(),
    images: createRandomStr(),
    collectedVideos: createRandomStr(),
    collectedImages: createRandomStr(),
    downloadedVideos: createRandomStr(),
    downloadedImages: createRandomStr(),
  };

  const needToDownloadVideosUrls = [];
  const needToDownloadImagesUrls = [];

  function createRandomNumber(min = 5, max = 30) {
    return Math.floor(Math.random() * (max - min + 1) + min)
  }

  function createRandomStr(len = createRandomNumber()) {
    let result = '';
    const string = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';

    for (let i = 0; i < len; i++) {
      result += string[createRandomNumber(0, string.length)];
    }

    return result;
  }

  const sleep = ms => new Promise(r => setTimeout(r, ms));

  /**
   * create a div
   * @param {string} element
   * @param {string} id
   * @param {string} className
   * @param {HTMLElement} parentDom if set, the created div will append to the parentDom
   * @param {object} style
   * @param {object} attributes
   */
  function createElement(element, id = '', className = '', parentDom = undefined, style= undefined , attributes = undefined) {
    const el = document.createElement(element);

    id && el.setAttribute('id', id);
    className && el.setAttribute('class', className);

    if (attributes) {
      Object.entries(attributes).forEach(([key, value]) =>{
        el.setAttribute(key, value);
      });
    }

    if (style) {
      Object.entries(style).forEach(([key, value]) =>{
        el.style[key] = value;
      });
    }

    if (parentDom) {
      parentDom.appendChild(el);
    }

    return el;
  }

  /**
   * get a element
   * @param {string} id
   * @returns {HTMLElement|null}
   */
  function getElement(id) {
    return document.getElementById(id);
  }

  /**
   * create or find a div
   * @param {string} element
   * @param {string} id
   * @param {[string, HTMLElement, object, object]} args
   */
  function createOrGetElement(element, id, ...args) {
    return getElement(id) || createElement(element, id, ...args);
  }

  /**
   * remove a element
   * @param {string} id
   */
  function remoteElement(id) {
    const el = document.getElementById(id);
    if (el) {
      el.parentElement.removeChild(el);
    }
  }

  /**
   * create an element and set its innerHTML
   * @param {string|number} text
   * @param {[string, string, string, HTMLElement, object, object]} args
   * @returns {HTMLElement}
   */
  function createElementAndSetInnerHtml(text, ...args) {
    const el = createElement(...args);

    el.innerHTML = text;

    return el;
  }

  /**
   * create a button
   * @param {string} id
   * @param {string} text
   * @param {HTMLElement} parentEl
   * @param {string} appendClassName
   * @returns {HTMLElement}
   */
  function createButton(id, text, parentEl = undefined, appendClassName) {
    const button = createElement('button', id, 'm-rounded g-btn' + (appendClassName ? ` ${appendClassName}` : ''), parentEl);
    button.innerHTML = text;

    return button;
  }

  /**
   * create a button with wrapper
   * @param {string} id
   * @param {string} text
   * @param {HTMLElement} parentEl
   * @param {string} buttonAppendClassName
   * @returns {HTMLElement} the button element
   */
  function createButtonWithWrapper(id, text, parentEl = undefined, buttonAppendClassName = '') {
    const div = createElement('div', '', 'b-offer-wrapper', parentEl);

    return createButton(id, text, div, buttonAppendClassName);
  }

  /**
   * create a label with checkbox inside
   * @param {string} id
   * @param {boolean} checked
   * @param {string} labelText
   * @param {string} description
   * @param {HTMLElement} parentNode
   * @returns {HTMLElement}
   */
  function createCheckboxWithLabel(id, checked, labelText, description, parentNode) {
    const label = createElement('label', '', 'b-users-lists__item__label m-collection-item b-input-radio__container');
    label.innerHTML = `<input id="${id}" type="checkbox" class="b-input-radio m-only-by-class">
    <label for="${id}" class="b-input-radio__label">
      <svg class="g-icon" aria-hidden="true">
        <use xlink:href="#icon-done" href="#icon-done"></use>
      </svg>
      <span class="b-input-ripple"></span>
    </label>
    <div class="b-users-lists__item m-with-rectangle-hover m-tb-sm m-border-full-width">
      <div class="b-users-lists__item__text">
        <div class="b-users-lists__item__name m-size-sm-text"> ${labelText}</div>
        <div class="b-users-lists__item__count"> ${description}</div>
      </div>
    </div>`;

    parentNode.appendChild(label);

    /**
     * @type {HTMLInputElement|null}
     */
    const input = getElement(id);

    input.addEventListener('change', (e) => {
      if (e.target.checked) {
        input.classList.add('m-checked');
      } else {
        input.classList.remove('m-checked');
      }
    });

    if (checked) {
      input.click();
    }
  }

  /**
   * check if checkbox checked
   * @param id
   * @returns {boolean}
   */
  function isCheckboxChecked(id) {
    /**
     * @type {HTMLInputElement}
     */
    const checkbox = getElement(id);

    return checkbox.checked;
  }

  /**
   * create an item block
   * @param {string} title
   * @param {HTMLElement} parentEl
   * @param {string} contentClassName
   * @param {string} wrapperAppendClassName
   * @returns {HTMLElement}
   */
  function createItemBlock(title, parentEl, contentClassName = 'b-offer-join', wrapperAppendClassName = '') {
    const block = createElement('li', '', `b-profile__content__item${wrapperAppendClassName ? ' ' + wrapperAppendClassName : ''}`, parentEl);

    const listOffers = createElement('div', '', 'list-offers m-offer-bottom-gap-reset', block);
    const wrapper = createElement('div', '', 'b-offer-wrapper m-start-campaign m-started-campaign', listOffers);
    const titleDiv = createElement('div', '', 'b-profile__content__item__title', wrapper);
    const linkDiv = createElement('div', '', 'b-profile__bio__link', titleDiv);
    linkDiv.innerHTML = title;

    return createElement('div', '', contentClassName, wrapper);
  }

  function createStatusBlock(parentEl) {
    const content = createItemBlock('Status', parentEl);
    const line1 = createElement('span', '', 'b-users__item__subscription-date', content);
    createElementAndSetInnerHtml('', 'span', statusId, 'b-users__item__subscription-date__label', line1);
    setCurrentStatus('Waiting to start');
  }

  function setCurrentStatus(text) {
    getElement(statusId).innerText = text;
  }

  /**
   * create check block
   * @param parentEl
   */
  function createCheckBlock(parentEl) {
    const content = createItemBlock('Settings', parentEl, 'b-offer-join', 'b-users-lists m-collections-list m-native-custom-scrollbar m-scrollbar-y m-invisible-scrollbar');

    createCheckboxWithLabel(scrollToEndCheckboxId, true, 'Scroll To End', 'Scroll to End to load all media', content);
    createCheckboxWithLabel(videoIncludeCheckboxId, true, 'Include Video', 'Download all videos', content);
    createCheckboxWithLabel(imageIncludeCheckboxId, true,'Include Image', 'Download all images', content);
  }

  function setInfoBlock(parentEl) {
    const content = createItemBlock('Information', parentEl);

    const line1 = createElement('span', '', 'b-users__item__subscription-date', content);
    const line2 = createElement('span', '', 'b-users__item__subscription-date', content);
    const line3 = createElement('span', '', 'b-users__item__subscription-date', content);
    const line4 = createElement('span', '', 'b-users__item__subscription-date', content);
    const line5 = createElement('span', '', 'b-users__item__subscription-date', content);
    const line6 = createElement('span', '', 'b-users__item__subscription-date', content);

    createElementAndSetInnerHtml('Recognized Videos:', 'span', '', 'b-users__item__subscription-date__label', line1);
    createElementAndSetInnerHtml(data.videos, 'span', dataIds.videos, 'b-users__item__subscription-date__value', line1);

    createElementAndSetInnerHtml('Recognized Images:', 'span', '', 'b-users__item__subscription-date__label', line2);
    createElementAndSetInnerHtml(data.images, 'span', dataIds.images, 'b-users__item__subscription-date__value', line2);

    createElementAndSetInnerHtml('Collected Videos:', 'span', '', 'b-users__item__subscription-date__label', line3);
    createElementAndSetInnerHtml(data.collectedVideos, 'span', dataIds.collectedVideos, 'b-users__item__subscription-date__value', line3);

    createElementAndSetInnerHtml('Collected Images:', 'span', '', 'b-users__item__subscription-date__label', line4);
    createElementAndSetInnerHtml(data.collectedImages, 'span', dataIds.collectedImages, 'b-users__item__subscription-date__value', line4);

    createElementAndSetInnerHtml('Downloaded Videos:', 'span', '', 'b-users__item__subscription-date__label', line5);
    createElementAndSetInnerHtml(data.downloadedVideos, 'span', dataIds.downloadedVideos, 'b-users__item__subscription-date__value', line5);

    createElementAndSetInnerHtml('Downloaded Images:', 'span', '', 'b-users__item__subscription-date__label', line6);
    createElementAndSetInnerHtml(data.downloadedImages, 'span', dataIds.downloadedImages, 'b-users__item__subscription-date__value', line6);
  }

  function createDownloadBlock(parentEl) {
    const content = createItemBlock('Methods', parentEl, 'b-tab-container');
    const wrapper = createElement('div', '', 'b-offer-wrapper', content);

    const downloadStartButton = createButtonWithWrapper(downloadButtonId, 'Start Download', wrapper);
    const forceStartDownload = createButtonWithWrapper(forceStartDownloadButtonId, 'Force To Start Download', wrapper, 'm-danger');
    const closeButton = createButtonWithWrapper(closeButtonId, 'Close Downloader', wrapper, 'm-danger');

    downloadStartButton.addEventListener('click', () => {
      download().then(() => {
        //
      });
    });

    forceStartDownload.addEventListener('click', () => {
      isForceStartToDownload = true;
    });

    closeButton.addEventListener('click', () => {
      remoteElement(parentId);
    })
  }

  function scrollToEnd() {
    let retryTimes = 0;
    if (isCheckboxChecked(scrollToEndCheckboxId)) {
      return new Promise((resolve) => {
        function smoothScroll() {
          setCurrentStatus('Scrolling to the end');
          if (isForceStartToDownload) {
            return resolve();
          }
          const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
          const clientHeight = document.documentElement.clientHeight;
          const scrollHeight = document.documentElement.scrollHeight;
          if (scrollHeight - 10 > currentScroll + clientHeight) {
            window.requestAnimationFrame(smoothScroll);
            window.scrollTo(0, currentScroll + (scrollHeight - currentScroll - clientHeight) / 2);

            setInterval(() => {
              const loadingContainer = document.getElementsByClassName('infinite-loading-container');
              if (loadingContainer && loadingContainer[0]) {
                if (loadingContainer[0].firstElementChild.style.display === 'none') {
                  return resolve();
                }
              }
              if (document.documentElement.scrollHeight > scrollHeight) {
                retryTimes = 0;
                smoothScroll();
              } else {
                if (retryTimes < scrollToEndMaxRetryTimes) {
                  retryTimes ++;
                  smoothScroll();
                } else {
                  return resolve();
                }
              }
            }, 1500);
          }
        }

        smoothScroll();
      });
    }
  }

  function switchToMediaPage() {
    const tab = document.querySelector('#content > div.l-wrapper > div.l-wrapper__content > div.l-profile-container > div > ul > li:nth-child(2) > a');
    tab && tab.click();
  }

  /**
   *
   * @param {HTMLElement} item
   * @returns {HTMLElement}
   */
  async function openMedia(item) {
    item.click();
    const container = await waitToGetElementByClassName(document.body, 'pswp__container');
    return container.children[1];
  }

  async function closeDialog() {
    const button = await waitToGetElementByClassName(document.body, 'pswp__button--close');
    button.click();
  }

  /**
   * @param {HTMLElement} wrapper
   * @param {string} tag
   * @returns {Promise<HTMLElement>}
   */
  async function waitToGetElementByTag(wrapper, tag) {
    while(1) {
      const target = wrapper.getElementsByTagName(tag);
      if (target && target[0]) {
        return target[0];
      } else {
        await sleep(200);
      }
    }
  }

  /**
   * @param {HTMLElement} wrapper
   * @param {string} className
   * @returns {Promise<HTMLElement>}
   */
  async function waitToGetElementByClassName(wrapper, className) {
    while(1) {
      const target = wrapper.getElementsByClassName(className);
      if (target && target[0]) {
        return target[0];
      } else {
        await sleep(200);
      }
    }
  }

  /**
   * @param {HTMLElement[]} videos
   */
  async function downloadVideos(videos) {
    for (let i = 0; i < videos.length; i++){
      setCurrentStatus(`Collect ${i + 1} video(s)`);
      data.collectedVideos++;
      const item = await openMedia(videos[i]);
      const tag = await waitToGetElementByTag(item, 'video');
      needToDownloadVideosUrls.push(tag.getAttribute('src'));
      await closeDialog();
      await sleep(sleepMs);
    }
  }

  /**
   * @param {HTMLElement[]} images
   */
  async function downloadImages(images) {
    for (let i = 0; i < images.length; i++){
      setCurrentStatus(`Collect ${i + 1} image(s)`);
      data.collectedImages++;
      const item = await openMedia(images[i]);
      const tag = await waitToGetElementByTag(item, 'img');
      needToDownloadImagesUrls.push(tag.getAttribute('src'));
      await closeDialog();
      await sleep(sleepMs);
    }
  }

  async function collectMedia() {
    const includeVideo = isCheckboxChecked(videoIncludeCheckboxId);
    const includeImage = isCheckboxChecked(imageIncludeCheckboxId);
    const videos = [];
    const images = [];

    const list = document.querySelector(listWrapperSelector);

    if (list) {
      for (const item of list.children) {
        if (item.classList.contains('m-video-item')) {
          if (includeVideo) {
            setCurrentStatus('Recognize a video');
            data.videos ++;
            videos.push(item);
          }
        } else if (item.classList.contains('b-photos__item')) {
          if (includeImage) {
            setCurrentStatus('Recognize a image');
            data.images ++;
            images.push(item);
          }
        }
      }

      if (videos.length) {
        await downloadVideos(videos);
      }

      if (images.length) {
        await downloadImages(images);
      }

      downloadAllUrls();
    } else {
      alert('Cannot find media list wrapper. Please check whether if you are subscribe to him.')
    }
  }

  function downloadAllUrls() {
    setCurrentStatus(`Download media`);
    for (let i = 0; i < needToDownloadVideosUrls.length; i++){
      GM_download({
        url: needToDownloadVideosUrls[i],
        name: window.document.title + '_' + (i + 1) + '.mp4',
        saveAs: false,
        onload: () => {
          data.downloadedVideos++;
        },
      });
    }

    for (let i = 0; i < needToDownloadImagesUrls.length; i++){
      GM_download({
        url: needToDownloadImagesUrls[i],
        name: window.document.title + '_' + (i + 1) + '.jpg',
        saveAs: false,
        onload: () => {
          data.downloadedImages++;
        },
      });
    }
  }

  function initData() {
    setCurrentStatus('Init data');
    data.videos = 0;
    data.images = 0;
    data.collectedVideos = 0;
    data.collectedImages = 0;
    data.downloadedImages = 0;
    data.downloadedVideos = 0;
    isForceStartToDownload = false;
    needToDownloadVideosUrls.splice(0, needToDownloadVideosUrls.length);

    if (!hasSetProxy) {
      data = new Proxy(data, {
        set(target, p, value) {
          const el = getElement(dataIds[p]);

          if (el) {
            el.innerText = value;
          }

          Reflect.set(target, p, value);
        }
      });
      hasSetProxy = true;
    }
  }

  async function download() {
    initData();
    switchToMediaPage();
    await sleep(500);
    await scrollToEnd();
    await collectMedia();
    setCurrentStatus(`Download success!`);
  }

  function init() {
    const parentElStyle = {
      width: '300px',
      minHeight: 'auto',
      padding: '10px',
      background: '#FFF',
      position: 'fixed',
      top: 'inherit',
      left: '30px',
      bottom: '30px',
      borderRadius: '6px',
      zIndex: 1000
    };
    const parentEl = createElement('div', parentId, 'l-wrapper__sidebar b-profile-list b-bundles-group', document.body, parentElStyle);
    const list = createElement('ul', '','b-profile__content__list', parentEl);

    createStatusBlock(list);
    createCheckBlock(list);
    setInfoBlock(list);
    createDownloadBlock(list);
  }

  function run(){
    init();
  }

  run();
})()