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.

🔴 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 * Download the extension 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 = ""; // 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 = ""; // 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 = ` Click here to update Fansly Downloader!Your Version: ${currentVer} - Latest Version: ${latestVer}`; 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); } }