sewageshep / ZXDL Master

// ==UserScript==
// @name         ZXDL Master
// @namespace    http://zoox18.com/
// @version      1.6.0
// @description  View and download private videos and pictures from ZX18.
// @author       Narkuh, refactored by MiloGShep
// @license      MIT
// @match        http*://*.zoox18.com/*
// @connect      pastebin.com
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @require      http://code.jquery.com/jquery-3.4.1.min.js
// @require      https://cdn.plyr.io/3.5.6/plyr.js
// ==/UserScript==

(function ($) {
  "use strict";
  const zxdl = {
    VIDEO_ID: window.location.pathname.split("/")[2],
    key0: "",
    key1: "",
    key2: "",
    paths: [],
    isDownloading: false,
    isVideoFound: false,
    isPrivateWindow: false,
    videoUrl: "",
    videoUploader: "Unknown",
    videoTitle: "Unknown",
    videoDescription: "None",
    linkCheckThisSession: false,
  };

  function removeAnnoyance() {
    document.querySelectorAll(".img-private").forEach(function(element) {
        element.style.filter = "brightness(1)";
    });
    document.querySelectorAll(".label-private").forEach(function(element) {
        element.style.filter = "opacity(0.5)";
    });
  }

  function makeNavigationBar() {
    $(".navbar").after('<div class="container" id="rip-div" style="width: 560px;"></div>');
    $(".top-menu > .pull-left").append(
      '<li id="zxdl-header"><span id="zx-ver">ZXDL 1.5.3</span><span><a data-toggle="modal" href="#zxdl-modal"><span class="caret"></span></span></a></li>'
    );
    $("body").append(
      '<div class="modal fade in" id="zxdl-modal"><div class="modal-dialog zxdl-modal"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">テ�</button> <h4 class="modal-title">ZXDL</h4> </div> <div class="modal-body">Version 1.5.3 | by Lowfo and Narkuh<br><span class="link-status"></span></div></div></div></div>'
    );
  }

  function formatBytes(bytes, decimalPlaces = 2) {
    if (bytes === 0) return "0 bytes";
    const dp = 0 > decimalPlaces ? 0 : decimalPlaces,
      sizeIndex = Math.floor(Math.log(bytes) / Math.log(1024));
    return parseFloat((bytes / Math.pow(1024, sizeIndex)).toFixed(dp)) + " " + ["bytes", "KB", "MB", "GB"][sizeIndex];
  }

  function calculateAndShowDownloadProgress(xmlHttpRequestProgressEvent) {
    if (xmlHttpRequestProgressEvent.lengthComputable === false) return;
    $("#dl-data").html(
      formatBytes(xmlHttpRequestProgressEvent.loaded) + " / " + formatBytes(xmlHttpRequestProgressEvent.total)
    );
    $("#dl-bar").attr(
      "aria-valuenow",
      Math.floor((xmlHttpRequestProgressEvent.loaded / xmlHttpRequestProgressEvent.total) * 100)
    );
    $("#dl-bar").css(
      "width",
      Math.floor((xmlHttpRequestProgressEvent.loaded / xmlHttpRequestProgressEvent.total) * 100) + "%"
    );
    $("#dl-bar").html(Math.floor((xmlHttpRequestProgressEvent.loaded / xmlHttpRequestProgressEvent.total) * 100) + "%");
  }

  function showDownloadSuccess(xmlhttpRequestProgressEvent) {
    if (xmlhttpRequestProgressEvent.lengthComputable === false) return;
    $("#dl-data").html("Complete!");
    $("#dl-bar").addClass("progress-bar-success");
  }

  function showDownloadError(xmlhttpRequestProgressEvent) {
    if (xmlhttpRequestProgressEvent.lengthComputable === false) return;
    $("#dl-data").html("Oops, there was an error. Refresh page to try again");
    $("#dl-bar").addClass("progress-bar-danger");
  }

  function sendGetRequestWithCallback(url, callback) {
    $.ajax({
      url: url,
      dataType: "jsonp",
      type: "GET",
      complete: function (xhr) {
        if (typeof callback === "function") {
          callback.apply(this, [xhr.status]);
        }
      },
    });
  }

  function checkMenuLinksStatus() {
    const OK = 200;
    if (!zxdl.linkCheckThisSession) {
      zxdl.linkCheckThisSession = true;
      for (let i = 0; i < zxdl.paths.length; i++) {
        let baseUrl = zxdl.paths[i].substring(0, zxdl.paths[i].lastIndexOf("/"));
        $("#zxdl-modal .link-status").append('<div class="link-' + i + '">Link ' + (i + 1) + ":</div>");
        sendGetRequestWithCallback(baseUrl, function (status) {
          if (status === OK) {
            $("#zxdl-modal .link-" + i).append(' <i class="fa fa-check"></i>');
          }
          else {
            $("#zxdl-modal .link-" + i).append(' <i class="fa fa-times"></i>');
          }
        });
      }
    }
  }

  function scanAndLoadVideoFromUrl(url) {
    if (zxdl.isVideoFound == false) {
      let videoElement = document.createElement("VIDEO");
      videoElement.addEventListener("loadeddata", function () {
        console.log("ZXDL: Video found! " + url);
        zxdl.isVideoFound = true;
        zxdl.videoUrl = url;

        if (zxdl.isPrivateWindow) {
          $("#rip-div").html(
            "<h1>" +
            zxdl.videoTitle +
            '</h1><p>Uploaded by <a href="https://zoox18.com/user/' +
            zxdl.videoUploader +
            '">' +
            zxdl.videoUploader +
            '</a></p><p style="opacity:0.5">"' +
            zxdl.videoDescription +
            '"</p><p style="font-size:12px">(' +
            zxdl.videoUrl +
            ')</p><link rel="stylesheet" href="https://cdn.plyr.io/3.5.6/plyr.css" /><video style="width: 100%; height: 100%;" poster="https://www.zoox18.com/media/videos/tmb1/' +
            zxdl.VIDEO_ID +
            '/default.jpg" id="rippedvid" playsinline controls><source src="' +
            zxdl.videoUrl +
            '" type="video/mp4" /></video><div><hr><button id="zxdl_favorite" class="btn btn-primary"><i class="glyphicon glyphicon-heart"></i> Favorite</button> <button id="zxdl_download" class="btn btn-primary"><i class="glyphicon glyphicon-download"></i> Download</button><p id="status"></p><div id="dl-progress" class="well" style="display: none"></div></div>'
          );

          $("#zxdl_favorite").click(function () {
            $("#status").html("Please wait...");
            const http = new XMLHttpRequest();
            const favoriteVideoBaseUrl = "https://www.zoox18.com/ajax/favorite_video";
            const form = "video_id=" + zxdl.VIDEO_ID;
            http.open("POST", favoriteVideoBaseUrl, true);
            http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            http.onreadystatechange = function () {
              if (http.readyState == 4 && http.status == 200) {
                const response = http.responseText;
                if (response.includes("alert-danger")) {
                  $("#status").html(
                    "Couldn't favorite video. Are you logged in? Is this video already in your favorites?"
                  );
                }
                else if (response.includes("alert-success")) {
                  $("#status").html('<span style="color:#77b300">Added to favorites!</span>');
                }
                else {
                  $("#status").html("The site returned unknown data.");
                }
              }
            };
            http.send(form);
          });
        }
        else {
          $("div#share_video").append(
            '<button id="zxdl_download" class="btn btn-primary m-l-5"><i class="glyphicon glyphicon-download"></i></button><p id="status"></p>'
          );
          $("#response_message").after('<div id="dl-progress" class="well" style="display: none"></div>');
          $("button.btn.btn-default.dropdown-toggle").remove();
        }

        $("#zxdl_download").click(function () {
          if (zxdl.isDownloading === false) {
            zxdl.isDownloading = true;
            $("#dl-progress").css("display", "block");
            $("#dl-progress").html(
              '<h4>Progress</h4><span id="dl-data">Loading...</span> <div class="progress"><div id="dl-bar" class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div></div>'
            );
            GM_download({
              url: url,
              name: zxdl.VIDEO_ID + ".mp4",
              onprogress: calculateAndShowDownloadProgress,
              onload: showDownloadSuccess,
              onerror: showDownloadError,
            });
          }
          else {
            alert("You've already initiated a download. Refresh the page to try again.");
          }
        });
      });
      videoElement.src = url;
    }
  }

  async function makeAllUrlListwithKey(getLinksToo) {
    return new Promise((resolve) => {
      try {
        GM_xmlhttpRequest({
          method: "GET",
          url: "https://pastebin.com/raw/pnEwmQ2f",
          headers: {
            "Cache-Control": "max-age=1200, must-revalidate",
          },
          onload: function (response) {
            let json = JSON.parse(response.responseText);
            if (getLinksToo) {
              zxdl.key0 = json.key0;
              zxdl.key1 = json.key1;
              zxdl.key2 = json.key2;
              zxdl.paths = [
                atob(json.host0) + zxdl.key0 + "/media/videos/h264/" + zxdl.VIDEO_ID + "_SD.mp4",
                atob(json.host1) + zxdl.key1 + "/media/videos/h264/" + zxdl.VIDEO_ID + "_SD.mp4",
                atob(json.host2) + zxdl.key2 + "/media/videos/iphone/" + zxdl.VIDEO_ID + ".mp4",
                atob(json.host2) + zxdl.key2 + "/media/videos/h264/" + zxdl.VIDEO_ID + "_SD.mp4",
              ];
              resolve();
            }
            if (json.revision) {
              $("#zx-ver").append("." + json.revision);
            }
            if (json.announcement) {
              $("#zxdl-header").append(
                '<div style="display:inline; width: 100%;font-size:11px;margin-left:10px;background: #252e51;padding: 2px 5px;color: white;"><i class="fa fa-info-circle"></i> ' +
                json.announcement +
                "</div>"
              );
            }
          },
        });
      }
      catch (err) {
        alert(
          "ZXDL: There was an error retrieving links for this session. Try refreshing the page, otherwise if you keep receiving this message, please contact us"
        );
      }
    });
  }

  function isPublicWindowActive() {
    return $("#wrapper .container .row .col-md-8 .vcontainer ").length > 0;
  }

  function isPrivateWindowActive() {
    return $("#wrapper .container .row .col-xs-12 .text-danger").length > 0;
  }

  function isVideoUrl() {
    return window.location.pathname.split("/")[1] == "video";
  }

  async function main() {
    if (isPrivateWindowActive()) {
      zxdl.isPrivateWindow = true;
      zxdl.videoUploader = $(".text-danger a").text(); // You must be friends with...
      zxdl.videoTitle = $("meta[property='og:title']").attr("content"); // Metatag still have title
      zxdl.videoDescription = $("meta[property='og:description']").attr("content"); // Metatag still have discription
      if (isVideoUrl()) {
        $(".well.well-sm").remove(); // Remove notice
        $(".well.ad-body").remove(); // Remove sponsor block for non-ad-blockers
        $("#rip-div").html("<h1>Scanning for video " + zxdl.VIDEO_ID + "...</h1><p>This can take up to a minute.</p>");
        await makeAllUrlListwithKey(true);
        zxdl.paths.forEach(scanAndLoadVideoFromUrl);
      }
    }
    else if (isPublicWindowActive()) {
      await makeAllUrlListwithKey(true);
      zxdl.paths.forEach(scanAndLoadVideoFromUrl);
    }
    else {
      await makeAllUrlListwithKey(false);
    }
  }

  function processAlbumPage() {
    if (document.URL.startsWith("https://www.zoox18.com/album/")) {
      const textElement = document.querySelector("div.text-danger");
      const parser = new DOMParser();

      function log(message) {
        textElement.innerText += `${message}\n`;
      }

      async function fetchAlbums(startID, albumPath = []) {
        log(`Trying album ${startID}...`);
        try {
          const response = await fetch(`https://www.zoox18.com/album/${startID}`, {
            mode: "same-origin"
          });
          if (!response.ok) return null;
          const responseText = await response.text();
          if (responseText) {
            const doc = parser.parseFromString(responseText, "text/html");
            const albumName = doc.querySelector(".col-md-8 .panel-heading .pull-left").innerText.trim();
            const imgs = doc.querySelectorAll('img[id^="album_photo"]');
            if (imgs.length > 0) {
              albumPath.push({
                id: startID,
                name: albumName,
                lastPhotoID: Number(imgs[imgs.length - 1].id.split("_")[2])
              });
              return albumPath;
            }
            else {
              albumPath.push({
                id: startID,
                name: albumName
              });
            }
          }
          return fetchAlbums(startID - 1, albumPath);
        }
        catch (error) {
          console.error(error);
          log("Error fetching album.");
          return null;
        }
      }

      async function fetchAlbumInfo(albumPath, photoCount = 0) {
        for (let i = 0; i < albumPath.length; i++) {
          const album = albumPath[i];
          log(`Trying to get info for ${album.id}...`);
          try {
            const response = await fetch(`https://www.zoox18.com/search/photos?search_query=${encodeURIComponent(album.name)}`, {
              mode: "same-origin"
            });
            if (!response.ok) continue;
            const responseText = await response.text();
            const doc = parser.parseFromString(responseText, "text/html");
            const imgs = doc.querySelectorAll('img[src^="/media/albums"]');
            imgs.forEach(img => {
              const id = Number(img.src.split("/")[5].split(".")[0]);
              if (id === album.id) {
                photoCount += Number(img.closest(".video-views").innerText.trim().split(" ")[0]);
              }
            });
          }
          catch (error) {
            console.error(error);
            log(`Error fetching album info for ${album.id}.`);
          }
        }
        return photoCount;
      }

      async function findPrivatePhoto(closestPhotoID, privateAlbumName) {
        log(`Trying photo ${closestPhotoID}...`);
        try {
          const response = await fetch(`https://www.zoox18.com/photo/${closestPhotoID}`, {
            mode: "same-origin"
          });
          if (!response.ok) return null;
          const responseText = await response.text();
          const doc = parser.parseFromString(responseText, "text/html");
          const albumTitle = doc.querySelector(".col-md-8 .panel-heading .pull-left").innerText.trim().substring(13);
          if (albumTitle === privateAlbumName) return closestPhotoID;
          return findPrivatePhoto(closestPhotoID + 1, privateAlbumName);
        }
        catch (error) {
          console.error(error);
          log(`Error finding private photo.`);
          return null;
        }
      }

      function displayPhotos(startID, photoCount) {
        const panelBody = textElement.parentNode;
        textElement.remove();
        let html = '<div class="row">';
        for (let i = 0; i < photoCount; i++) {
          const id = startID + i;
          html += `
          <div class="col-sm-4 m-t-15">
            <a href="/media/photos/${id}.jpg">
              <img src="/media/photos/tmb/${id}.jpg" class="img-responsive">
            </a>
          </div>
        `;
        }
        html += "</div>";
        panelBody.insertAdjacentHTML("afterbegin", html);
      }

      async function init() {
        if (textElement && confirm("Get private images from this album? It can take a minute.")) {
          textElement.innerText = "";
          const albumID = Number(document.URL.split("/")[4]);
          const albumName = document.querySelector(".col-md-8 .panel-heading .pull-left").innerText.trim();
          try {
            const albumPath = await fetchAlbums(albumID - 1);
            let closestPhotoID = albumPath[albumPath.length - 1]?.lastPhotoID + 1 || albumID;
            albumPath.pop();
            const photoCount = await fetchAlbumInfo(albumPath);
            closestPhotoID += photoCount;
            const privatePhotoID = await findPrivatePhoto(closestPhotoID, albumName);
            log("Got private ID, displaying photos...");
            const finalPhotoCount = await fetchAlbumInfo([{
              id: albumID,
              name: albumName
            }]) || 21;
            displayPhotos(privatePhotoID, finalPhotoCount);
          }
          catch (error) {
            console.error(error);
            log("Error during initialization.");
          }
        }
      }

      init();
    }
    document.querySelectorAll(".img-private").forEach(img => img.classList.remove("img-private"));
  }

  processAlbumPage()
  makeNavigationBar();
  removeAnnoyance();
  $("a[href='#zxdl-modal']").click(function () {
    checkMenuLinksStatus();
  });
  window.onload = main();
})(jQuery);