Full Code of Motyldrogi/fansly-downloader for AI

main 356720ceba82 cached
4 files
18.1 KB
5.0k tokens
1 symbols
1 requests
Download .txt
Repository: Motyldrogi/fansly-downloader
Branch: main
Commit: 356720ceba82
Files: 4
Total size: 18.1 KB

Directory structure:
gitextract_as8hcrs2/

├── README.md
├── content.js
├── manifest.json
└── style.css

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# Downloader for fansly.com
🟢 This is a Google Chrome extension that adds download buttons to fansly feed and image gallerys. It can download images, animated images (gifs), audio and videos.<br><br>
🔴 I don't know if downloading for private use is allowed, so use it at your own responsibility. But obviously you are not allowed to reproduce, publish, or distribute any content downloaded. Please do not do this.

## Installation

### Manual Installation
* <a href="https://github.com/Motyldrogi/fansly-downloader/releases/latest" target="_blank">Download the extension</a> and unzip it to a folder.
* Then go to the extension page manually or with url **chrome://extensions/** and turn on Developer Mode in the top right corner. In the new menu select "Load unpacked" and select the folder with the unzipped files.

### Webstore
* 🔴 EXTENSION GOT REMOVED FROM WEBSTORE "This functionality is not allowed per Chrome Web Store policies." 🔴

## Best quality download:
* Open an image and click on the dots icon at the top right corner and select "High Quality Media". The quality option for images is persistent. Videos get always downloaded in the selected quality (gear icon).
* Now click the download button at the top left to download the file.

## Notice:
* You need to be followed/subscribed to the fansly.com creator.
* No paywall is bypassed.
* No unauthorized access or download is encouraged, facilitated or enabled.

## Features:
* Download fansly.com videos.
* Download fansly.com images/photos/gifs/audio.
* Also works for profile pic, banner and private messages.

## Disclaimer:
"Fansly" or fansly.com is operated by Select Media LLC as stated on their "Contact" page. This Chrome extension (Downloader for fansly.com) isn't in any way affiliated with, sponsored by, or endorsed by Select Media LLC or "Fansly".


================================================
FILE: content.js
================================================
const addDownloadButtonToModal = (modalItem, node) => {
  const closeButton = modalItem.getElementsByClassName("modal-close-button")[0];

  // Private message video
  if (node.classList.contains("video-element-wrapper")) {
    node = node.querySelector(".video");
  }
  if (node.closest(".preview")) {
    return;
  }

  // If already added or not found return
  if (node.parentNode.querySelector(".modal-download-button") != null) {
    return;
  }

  if (closeButton != null) {
    // Add new download button after node
    const button = buildDownloadButtonModal(closeButton);
    button.addEventListener("click", onDownloadClickModal);

    setButtonVisibility(button);

    node.parentNode.insertBefore(button, node.nextSibling);
  }
};

const setButtonVisibility = (button) => {
  // Make the button visible
  button.style.setProperty("display", "flex", "important");
  button.style.setProperty("opacity", "1", "important");
};

const buildDownloadButtonModal = (closeButton) => {
  // Create new div button
  const button = document.createElement("div");
  button.classList.add(...closeButton.classList);
  button.classList.add("modal-download-button", "modal-pulse");

  // Copy ngcontent for styles
  button.setAttribute(closeButton.attributes[0].name, closeButton.attributes[0].value);

  // Add download icon to container
  const buttonIcon = document.createElement("div");
  buttonIcon.classList.add("download-icon");
  buttonIcon.innerHTML =
    "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-download'><path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'></path><polyline points='7 10 12 15 17 10'></polyline><line x1='12' y1='15' x2='12' y2='3'></line></svg>";

  // Copy ngcontent for styles
  buttonIcon.setAttribute(closeButton.attributes[0].name, closeButton.attributes[0].value);
  button.appendChild(buttonIcon);

  return button;
};

const getPathInfo = (event) => {
  const path = event.path || (event.composedPath && event.composedPath());
  if (!path) {
    console.log("Unable to get path information from browser.");
    return;
  }
  return path;
};

const onDownloadClickModal = (event) => {
  // Get image or video relative to button

  const path = getPathInfo(event);

  const downloadLink = path[2].querySelector("video")?.src || path[2].querySelectorAll(".image")[1]?.src || path[2].querySelectorAll(".image")[0]?.src;

  const feedUsername = "fansly";

  if (downloadLink != null && !downloadLink.includes("mp4")) {
    fetch(downloadLink)
      .then((res) => res.text())
      .then((data) => {
        const type = getTypeFromBlobStart(data.slice(0, 10));

        const name = feedUsername + "-" + Math.random().toString(36).substr(2) + type;

        downloadFile(downloadLink, name);
      });
  } else if (downloadLink != null && downloadLink.includes("mp4")) {
    const name = feedUsername + "-" + downloadLink.split("/")[4].split("?")[0];

    downloadFile(downloadLink, name);
  }
};

const addDownloadButtonToFeed = (feedItem) => {
  const hasMedia = hasItemMedia(feedItem);
  if (hasMedia == false) {
    return;
  }

  const lastElem = feedItem.getElementsByClassName("feed-item-stats").length - 1;
  const stats = feedItem.getElementsByClassName("feed-item-stats")[lastElem];

  // If already added or not found return
  if (!stats || stats.querySelectorAll(".download").length > 0) {
    return;
  }

  const tipsButton = stats.getElementsByClassName("tips")[0];
  if (tipsButton != null) {
    // Add new download button after tip button
    const button = buildDownloadButtonFeed(tipsButton);
    button.addEventListener("click", onDownloadClickFeed);

    setButtonVisibility(button);

    tipsButton.parentNode.insertBefore(button, tipsButton.nextSibling);
  }
};

const buildDownloadButtonFeed = (tipsButton) => {
  // Create new div button
  const button = document.createElement("div");
  button.classList.add(...tipsButton.classList);
  button.classList.replace("tips", "download");

  // Copy ngcontent for styles
  button.setAttribute(tipsButton.attributes[0].name, tipsButton.attributes[0].value);

  // Create new icon container
  const buttonIconContainer = document.createElement("div");
  buttonIconContainer.classList.add(...tipsButton.children[0].classList);
  buttonIconContainer.classList.replace("green", "pink");

  // Copy ngcontent for styles
  buttonIconContainer.setAttribute(tipsButton.attributes[0].name, tipsButton.attributes[0].value);
  button.appendChild(buttonIconContainer);

  // Add text
  button.appendChild(document.createTextNode("Download"));

  // Add download icon to container
  const buttonIcon = document.createElement("div");
  buttonIcon.classList.add("download-icon");
  buttonIcon.innerHTML =
    "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-download'><path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'></path><polyline points='7 10 12 15 17 10'></polyline><line x1='12' y1='15' x2='12' y2='3'></line></svg>";

  // Copy ngcontent for styles
  buttonIcon.setAttribute(tipsButton.attributes[0].name, tipsButton.attributes[0].value);
  buttonIconContainer.appendChild(buttonIcon);

  return button;
};

const onDownloadClickFeed = (event) => {
  // Stop angular click events
  event.stopPropagation();

  const path = getPathInfo(event);

  const feedItemContent = path[2].closest(".feed-item-content");

  const preview = feedItemContent.querySelectorAll(".feed-item-preview")[0];
  if (!preview.classList.contains("single-preview")) {
    preview.querySelectorAll(".image")[0].click();
    return;
  }

  downloadLinks = getBlobUrls(feedItemContent);

  const feedUsername = feedItemContent.querySelector(".display-name").textContent.replace(/\s+/g, "");

  // Check if image or video
  downloadLinks.forEach((downloadLink) => {
    if (downloadLink.startsWith("img:")) {
      fetch(downloadLink.substr(4))
        .then((res) => res.text())
        .then((data) => {
          const type = getTypeFromBlobStart(data.slice(0, 10));

          const name = feedUsername + "-" + Math.random().toString(36).substr(2) + type;

          downloadFile(downloadLink.substr(4), name);
        });
    } else {
      const name = feedUsername + "-" + downloadLink.split("/")[4].split("?")[0];

      downloadFile(downloadLink.substr(4), name);
    }
  });
};

const getTypeFromBlobStart = (blobStr) => {
  let type = ".png";

  if (blobStr.includes("GIF")) {
    type = ".gif";
  } else if (blobStr.includes("JPG")) {
    type = ".jpg";
  } else if (blobStr.includes("JPEG")) {
    type = ".jpeg";
  } else if (blobStr.includes("ftyp")) {
    type = ".mp4";
  }

  return type;
};

const hasItemMedia = (feedItem) => {
  let returnable = true;
  const preview = feedItem.querySelector(".feed-item-preview");

  if (preview) {
    let count = 0;
    const images = preview.querySelectorAll(".image");

    for (let i = 0; i < images.length; i++) {
      const img = images[i].querySelector(".image");
      if (img != null && img.src != null) {
        count++;
      }
    }

    if (count == 0) {
      returnable = false;
    }
  } else if (!preview) {
    returnable = false;
  }

  return returnable;
};

const getBlobUrls = (feedItem) => {
  let returnable = [];

  const preview = feedItem.querySelector(".feed-item-preview");

  let video = false;
  if (preview.querySelectorAll("video").length > 0) {
    video = true;
  }

  const images = preview.querySelectorAll(".image");

  for (let i = 0; i < images.length; i++) {
    if (images[i].querySelector(".image") != null) {
      const img = images[i].querySelector(".image");

      if (img.src != null && !video) {
        returnable.push("img:" + img.src);
      } else {
        console.log(preview);
        const vid = preview.querySelector("video");
        if (vid) {
          returnable.push("vid:" + vid.src);
        }
      }
    }
  }

  return returnable;
};

const downloadFile = (url, name) => {
  // Download video
  if (url.includes("mp4")) {
    downloadVideo(url, name);
    return;
  }

  // Download image
  const link = document.createElement("a");
  link.href = url;

  link.setAttribute("download", name);
  document.body.appendChild(link);

  link.click();
  link.parentNode.removeChild(link);
};

const buildProgressItem = (name) => {
  // Create progress item
  const progressItem = document.createElement("div");
  progressItem.classList.add("item");
  progressItem.innerText = name;

  const progressBar = document.createElement("div");
  progressBar.classList.add("progress");

  const progressBarInner = document.createElement("div");
  progressBar.appendChild(progressBarInner);

  progressItem.appendChild(progressBar);

  return progressItem;
};

const updateProgressItem = ({ loaded, total }, progressItem) => {
  const percentage = Math.round((loaded / total) * 100) + "%";
  progressItem.style.width = percentage;
  progressItem.innerText = percentage;
};

const createProgressContainer = () => {
  // Create container for progress items
  const progressContainer = document.createElement("div");
  progressContainer.setAttribute("id", "progress-container");

  document.body.insertBefore(progressContainer, document.body.firstChild);
};

const finishedProgress = (progressItem, progressBar) => {
  // Remove progress item
  progressBar.innerText = "Finished download!";

  progressItem.style.transition = "opacity 1s ease-in 3s";

  progressItem.style.opacity = 0;
  setTimeout(() => {
    progressItem.parentNode.removeChild(progressItem);
  }, 4000);
};

const downloadVideo = async (url, name) => {
  const response = await fetch(url, {
    cache: "no-store",
    headers: new Headers({
      Origin: location.origin
    }),
    mode: "cors"
  });
  const contentLength = response.headers.get("content-length");
  const total = parseInt(contentLength, 10);
  let loaded = 0;

  const res = new Response(
    new ReadableStream({
      async start(controller) {
        const reader = response.body.getReader();

        // Add progress bar
        const progressItem = buildProgressItem(name);
        const progressContainer = document.getElementById("progress-container");
        progressContainer.insertBefore(progressItem, progressContainer.firstChild);

        // Update progress
        const progressBar = progressItem.querySelector(".progress").firstChild;
        for (;;) {
          const { done, value } = await reader.read();
          if (done) break;
          loaded += value.byteLength;
          updateProgressItem({ loaded, total }, progressBar);
          controller.enqueue(value);
        }
        controller.close();

        finishedProgress(progressItem, progressBar);
      }
    })
  );
  const blob = await res.blob();

  if (blob.size != 0) {
    let blobUrl = window.URL.createObjectURL(blob);

    const link = document.createElement("a");
    link.href = blobUrl;

    // Trigger download
    link.setAttribute("download", name);
    document.body.appendChild(link);

    link.click();
    link.parentNode.removeChild(link);
  }
};

const checkVersionUpdate = () => {
  var currentVer = chrome.runtime.getManifest().version;

  fetch("https://api.github.com/repos/Motyldrogi/fansly-downloader/releases/latest")
    .then((res) => res.json())
    .then((data) => {
      if (currentVer != data.tag_name) {
        createUpdateContainer(currentVer, data.tag_name);
      }
    });
};

const createUpdateContainer = (currentVer, latestVer) => {
  const container = document.createElement("div");
  container.setAttribute("id", "update-container");
  container.innerHTML = `<a href="https://github.com/Motyldrogi/fansly-downloader/releases" target="_blank">
    Click here to update Fansly Downloader!</a>Your Version: <b>${currentVer}</b> - Latest Version: <b>${latestVer}</b>`;
  document.body.insertBefore(container, document.body.firstChild);

  const dismiss = document.createElement("div");
  dismiss.setAttribute("class", "dismiss");
  dismiss.innerText = "X";
  dismiss.addEventListener("click", dismissUpdateContainer);
  container.appendChild(dismiss);
};

const dismissUpdateContainer = (event) => {
  const path = getPathInfo(event);
  const container = path[1];
  container.parentNode.removeChild(container);
};

const afterPageLoad = () => {
  // Init after page load
  createProgressContainer();
  checkVersionUpdate();
};

const observerCallback = (mutationsList) => {
  mutationsList.forEach((mutation) => {
    if (!mutation.addedNodes) return;

    for (let i = 0; i < mutation.addedNodes.length; i++) {
      const node = mutation.addedNodes[i];
      const classList = node.classList;

      if (classList == null) return;

      // Image was added
      if (classList.contains("image")) {
        const feedItem = node.closest(".feed-item-content");
        if (feedItem) {
          addDownloadButtonToFeed(feedItem);
        }

        const modalItem = node.closest(".active-modal");
        if (modalItem) {
          addDownloadButtonToModal(modalItem, node);
        }
      }
    }
  });
};

const config = {
  attributes: true,
  childList: true,
  subtree: true,
  characterData: true
};

const observer = new MutationObserver(observerCallback);

window.addEventListener("load", function () {
  observer.observe(document.body, config);
  afterPageLoad();
});


================================================
FILE: manifest.json
================================================
{
  "name": "Fansly.com Downloader",
  "description": "Adds a download button to fansly feed and image gallery.",
  "version": "2.0.6",
  "manifest_version": 3,
  "content_scripts": [{
    "matches": ["https://fansly.com/*"],
    "js": ["content.js"],
    "css" : ["style.css"],
    "all_frames": true
  }],
  "icons": {
    "16": "/images/icon-16.png",
    "32": "/images/icon-32.png",
    "48": "/images/icon-48.png",
    "128": "/images/icon-128.png"
  }
}


================================================
FILE: style.css
================================================
.modal-download-button {
  top: 25px !important;
  left: 190px !important;
  background-color: #1b1c21 !important;
  z-index: 999 !important;
  border-radius: 50% !important;
  width: 80px !important;
  height: 80px !important;
}

.modal-download-button .download-icon svg {
  stroke: rgb(244, 154, 255);
}

.modal-download-button:hover .download-icon svg {
  stroke: #c46fcf !important;
}

.download-icon {
  pointer-events: none;
}

@media (max-width: 700px) {
  .modal-download-button {
    left: 130px !important;
  }
}

.feed-item-stat.custom-hover-trigger .icon-container .download-icon svg {
  stroke: var(--dark-blue-1);
}

.feed-item-stat.custom-hover-trigger:hover .icon-container .download-icon svg {
  stroke: #f49aff;
}

.custom-hover-effect.pink:after {
  background-color: rgba(244, 154, 255, 0.137);
}

#progress-container {
  max-height: 200px;
  overflow: hidden auto;
  z-index: 999 !important;
  position: fixed;
  bottom: 50px;
  right: 50px;
  min-width: 300px;
}

#progress-container .item {
  background: #222;
  border: 1px solid var(--blue-2);
  border-radius: 6px;
  height: 50px;
  color: #fff;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  padding: 5px 10px 5px 5px;
  margin-bottom: 3px;
}

#progress-container .progress {
  background: #333;
  border-radius: 9px;
  height: 20px;
  width: 100%;
  padding: 3px;
}

#progress-container .progress > div {
  background: var(--blue-2);
  color: #fff;
  height: 100%;
  border-radius: 6px;
  text-align: center;
}

#update-container {
  position: fixed;
  top: 85px;
  background: #1b1c21;
  right: 20px;
  border-radius: 12px;
  padding: 15px;
  z-index: 3 !important;
  overflow: hidden auto;
  min-width: 320px;
  box-shadow: var(--box-shadow-1);
}

#update-container a {
  color: #2699f6;
  text-decoration: none;
  margin-bottom: 5px;
  display: block;
}

#update-container .dismiss {
  position: absolute;
  right: 20px;
  top: 15px;
  font-size: 16px;
  cursor: pointer;
  color: #fff;
  font-weight: 300;
  font-family: Arial, sans-serif;
}

.modal-pulse {
  box-shadow: 0 0 0 rgba(244, 154, 255, 0.4);
  animation: pulse 2s infinite;
  border: 1px solid rgba(244, 154, 255, 0.4);
}

@-webkit-keyframes pulse {
  0% {
    -webkit-box-shadow: 0 0 0 0 rgba(244, 154, 255, 0.4);
  }
  70% {
    -webkit-box-shadow: 0 0 0 20px rgba(244, 154, 255, 0);
  }
  100% {
    -webkit-box-shadow: 0 0 0 0 rgba(244, 154, 255, 0);
  }
}
@keyframes pulse {
  0% {
    -moz-box-shadow: 0 0 0 0 rgba(244, 154, 255, 0.4);
    box-shadow: 0 0 0 0 rgba(244, 154, 255, 0.4);
  }
  70% {
    -moz-box-shadow: 0 0 0 20px rgba(244, 154, 255, 0);
    box-shadow: 0 0 0 20px rgba(244, 154, 255, 0);
  }
  100% {
    -moz-box-shadow: 0 0 0 0 rgba(244, 154, 255, 0);
    box-shadow: 0 0 0 0 rgba(244, 154, 255, 0);
  }
}
Download .txt
gitextract_as8hcrs2/

├── README.md
├── content.js
├── manifest.json
└── style.css
Download .txt
SYMBOL INDEX (1 symbols across 1 files)

FILE: content.js
  method start (line 336) | async start(controller) {
Condensed preview — 4 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (20K chars).
[
  {
    "path": "README.md",
    "chars": 1826,
    "preview": "# Downloader for fansly.com\n🟢 This is a Google Chrome extension that adds download buttons to fansly feed and image gall"
  },
  {
    "path": "content.js",
    "chars": 13488,
    "preview": "const addDownloadButtonToModal = (modalItem, node) => {\n  const closeButton = modalItem.getElementsByClassName(\"modal-cl"
  },
  {
    "path": "manifest.json",
    "chars": 460,
    "preview": "{\n  \"name\": \"Fansly.com Downloader\",\n  \"description\": \"Adds a download button to fansly feed and image gallery.\",\n  \"ver"
  },
  {
    "path": "style.css",
    "chars": 2801,
    "preview": ".modal-download-button {\n  top: 25px !important;\n  left: 190px !important;\n  background-color: #1b1c21 !important;\n  z-i"
  }
]

About this extraction

This page contains the full source code of the Motyldrogi/fansly-downloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 4 files (18.1 KB), approximately 5.0k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!