Greasy Fork

Greasy Fork is available in English.

FR:Reborn - BaseTao

Upload Taobao and Yupoo QCs from BaseTao to Imgur + QC server

当前为 2021-06-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         FR:Reborn - BaseTao
// @namespace    https://www.reddit.com/user/RobotOilInc
// @version      0.5.1
// @description  Upload Taobao and Yupoo QCs from BaseTao to Imgur + QC server
// @author       RobotOilInc
// @match        https://www.basetao.net/index/myhome/myorder/*
// @match        https://basetao.net/index/myhome/myorder/*
// @match        https://www.basetao.com/index/myhome/myorder/*
// @match        https://basetao.com/index/myhome/myorder/*
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @license      MIT
// @homepageURL  https://www.qc-server.cf/
// @supportURL   http://greasyfork.icu/en/scripts/426977-fr-es-basetao
// @include      https://www.basetao.net/index/orderphoto/itemimg/*
// @include      https://basetao.net/index/orderphoto/itemimg/*
// @include      https://www.basetao.com/index/orderphoto/itemimg/*
// @include      https://basetao.com/index/orderphoto/itemimg/*
// @require      https://unpkg.com/sweetalert2@11/dist/sweetalert2.min.js
// @require      https://unpkg.com/[email protected]/src/logger.min.js
// @require      https://unpkg.com/[email protected]/spark-md5.min.js
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// @require      https://unpkg.com/@phamthaibaoduy/[email protected]/dist/jquery.ajax-retry.min.js
// @require      https://unpkg.com/[email protected]/dist/swagger-client.browser.min.js
// @require      http://greasyfork.icu/scripts/11562-gm-config-8/code/GM_config%208+.js?version=66657
// @resource     sweetalert2 https://unpkg.com/[email protected]/dist/sweetalert2.min.css
// @run-at       document-end
// @icon         https://i.imgur.com/1aQAxbC.png
// ==/UserScript==

class QC {
  /**
     * @param version {string}
     * @param client {SwaggerClient}
     * @param userHash {string}
     */
  constructor(version, client, userHash) {
    this.version = version;
    this.client = client;
    this.userHash = userHash;
  }

  /**
     * @param element
     * @returns {Promise<null|string>}
     */
  existingAlbumByOrderId(element) {
    return this.client.apis.QualityControl.hasUploaded({
      usernameHash: this.userHash,
      orderId: element.orderId,
    }).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

      if (!response.body.success) {
        return null;
      }

      return response.body.albumId;
    }).catch((err) => {
      Logger.error('Could not check if the album exists an album', err);

      return '-1';
    });
  }

  /**
     * @param url {string}
     * @returns {Promise<boolean>}
     */
  exists(url) {
    return this.client.apis.QualityControl.exists({ url }).then((response) => {
      if (typeof response.body === 'undefined') {
        return null;
      }

      if (!response.body.success) {
        return null;
      }

      return response.body.exists;
    }).catch((err) => {
      Logger.error('Could not check if the album exists an album', err);

      return false;
    });
  }

  /**
     * @param element {Element}
     * @param album {string}
     */
  uploadQc(element, album) {
    return this.client.apis.QualityControl.postQualityControlCollection({}, {
      method: 'post',
      requestContentType: 'application/json',
      requestBody: {
        usernameHash: this.userHash,
        albumId: album,
        color: element.color,
        orderId: element.orderId,
        purchaseUrl: element.url,
        sizing: element.size,
        itemPrice: element.itemPrice,
        freightPrice: element.freightPrice,
        source: `BaseTao to Imgur ${this.version}`,
        website: element.website,
      },
    });
  }
}

/**
 * @param text {string}
 * @param type {null|('success'|'error'|'warning'|'info')}
 */
const Snackbar = function (text, type = null) {
  if (typeof type !== 'undefined' && type != null) {
    Swal.fire({
      title: text,
      icon: type,
      position: 'bottom-end',
      showConfirmButton: false,
      allowOutsideClick: false,
      backdrop: false,
      timer: 1500,
    });

    return;
  }

  Swal.fire({
    title: text,
    position: 'bottom-end',
    showConfirmButton: false,
    allowOutsideClick: false,
    backdrop: false,
    timer: 1500,
  });
};

class Imgur {
  /**
     * @param version {string}
     * @param config {GM_config}
     */
  constructor(version, config) {
    this.version = version;

    if (config.get('imgurApi') === 'imgur') {
      this.headers = { authorization: `Client-ID ${config.get('imgurClientId')}` };
      this.host = config.get('imgurApiHost');

      return;
    }

    if (config.get('imgurApi') === 'rapidApi') {
      this.headers = {
        authorization: `Bearer ${config.get('rapidApiBearer')}`,
        'x-rapidapi-key': config.get('rapidApiKey'),
        'x-rapidapi-host': config.get('rapidApiHost'),
      };
      this.host = config.get('rapidApiHost');

      return;
    }

    throw new Error('Invalid Imgur API has been chosen');
  }

  /**
     * @param options
     * @returns {Promise<*|null>}
     */
  CreateAlbum(options) {
    return $.ajax({
      url: `https://${this.host}/3/album`,
      type: 'POST',
      headers: this.headers,
      data: { title: options.title, privacy: 'hidden' },
    }).retry({ times: 3 }).catch((err) => {
      // Log the error somewhere
      Logger.error(`Could not make an album: ${err.statusText}`, err);

      // If we uploaded too fast, tell the user
      if (err.responseJSON.data.error.code === 429) {
        Snackbar(`Imgur is telling us to slow down: ${err.responseJSON.data.error.message}`, 'error');
        return;
      }

      // Tell the user that "something" is wrong
      Snackbar('Could not make an album, please try again later...', 'error');
    });
  }

  /**
     * @param base64Image {string}
     * @param deleteHash {string}
     * @returns {Promise<*|null>}
     */
  async AddImageToAlbum(base64Image, deleteHash) {
    return $.ajax({
      url: `https://${this.host}/3/image`,
      headers: this.headers,
      type: 'POST',
      data: { album: deleteHash, type: 'base64', image: base64Image },
    }).retry({ times: 3 }).then(() => true).catch((err) => {
      // If we uploaded too many files, tell the user
      if (err.responseJSON.data.error.code === 429) {
        return false;
      }

      // Log the error otherwise
      Logger.error('An error happened when uploading the image', err);

      return false;
    });
  }

  RemoveAlbum(deleteHash) {
    $.ajax({ url: `https://${this.host}/3/album/${deleteHash}`, headers: this.headers, type: 'DELETE' }).retry({ times: 3 });
  }
}

class Element {
  constructor($element) {
    const $baseElement = $element.parents('tr').find('td[colspan=\'2\']').first();

    this.object = $element;
    this.url = $baseElement.find('.goodsname_color').first().attr('href').replace('http://', 'https://')
      .replace('?uid=1', '');
    this.title = $baseElement.find('.goodsname_color').first().text();
    this.size = $baseElement.find('.size_color_color:nth-child(2) > u').text();
    this.orderId = $element.parents('tr').prev().find('td.tdpd > span:nth-child(2)').text();
    this.itemPrice = $element.parents('tr').find('td:nth-child(2) > span').first().text();
    this.freightPrice = $element.parents('tr').find('td:nth-child(3) > span').first().text();

    const color = $baseElement.find('.size_color_color:nth-child(1) > u').text();
    if (color !== '-' && color !== 'NO') {
      this.color = color;
    }
  }

  /**
     * @returns {{color, size}}
     */
  get sizingInfo() {
    return { color: this.color, size: this.size };
  }

  /**
     * @param imageUrls {string[]}
     */
  set images(imageUrls) {
    this.imageUrls = imageUrls;
  }

  /**
     * @returns {boolean}
     */
  get isSupported() {
    return this.url.indexOf('item.taobao.com') !== -1
            || this.url.indexOf('yupoo.com') !== -1;
  }

  /**
     * @returns {string}
     */
  get website() {
    if (this.url.indexOf('item.taobao.com') !== -1) {
      return 'taobao';
    }

    if (this.url.indexOf('yupoo.com') !== -1) {
      return 'yupoo';
    }

    return 'unknown';
  }
}

const toDataURL = (url) => fetch(url)
  .then((response) => response.blob())
  .then((blob) => new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  }));

/**
 * @param base64Data {string}
 * @returns {Promise<string>}
 */
const WebpToJpg = function (base64Data) {
  return new Promise((resolve) => {
    const image = new Image();
    image.src = base64Data;

    image.onload = () => {
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');

      canvas.width = image.width;
      canvas.height = image.height;
      context.drawImage(image, 0, 0);

      resolve(canvas.toDataURL('image/jpeg'));
    };
  });
};

class Processor {
  /**
   * @param qcClient {QC}
   * @param imgurClient {Imgur}
   */
  constructor(qcClient, imgurClient) {
    this.qcClient = qcClient;
    this.imgurClient = imgurClient;
  }

  /**
   * @param element {Element}
   * @returns {Promise<void>}
   */
  async UploadToImgur(element) {
    const $processing = $('<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="https://i.imgur.com/lnFQTQz.gif" alt="Processing..."></span></li></ul>');
    const $base = element.object;
    $base.after($processing).hide();

    Snackbar('Pictures are being uploaded....');

    // Create the album
    const response = await this.imgurClient.CreateAlbum(element);
    if (typeof response === 'undefined' || response == null) {
      return;
    }

    const deleteHash = response.data.deletehash;
    const albumId = response.data.id;

    const AlbumLink = `https://imgur.com/a/${albumId}`;

    // Upload all QC images
    let uploadedImages = 0;
    const promises = [];
    $.each(element.imageUrls, (key, imageUrl) => {
      // Convert to base64, since Imgur cannot access our images
      promises.push(toDataURL(imageUrl).then(async (data) => {
        // Store our base64 and if the file is WEBP, convert it to JPG
        let base64Image = data;
        if (base64Image.indexOf('image/webp') !== -1) {
          base64Image = await WebpToJpg(base64Image);
        }

        // Remove the unnecessary `data:` part
        const cleanedData = base64Image.replace(/(data:image\/.*;base64,)/, '');

        // Upload the image to the album
        return this.imgurClient.AddImageToAlbum(cleanedData, deleteHash);
      }).then((uploaded) => {
        if (uploaded === false) {
          return;
        }

        uploadedImages++;
      }));
    });

    // Wait until everything has been tried to be uploaded
    await Promise.all(promises);

    // If not all images have been uploaded, abort everything
    if (uploadedImages !== element.imageUrls.length) {
      Snackbar('Imgur is rate-limiting you, try again later.', 'error');
      this.imgurClient.RemoveAlbum(deleteHash);

      $processing.remove();
      $base.show();

      return;
    }

    // Tell the user it was uploaded and open the album in the background
    Snackbar('Pictures have been uploaded!', 'success');
    GM_openInTab(AlbumLink, true);

    // Tell QC Suite about our uploaded QC's (if it's from TaoBao)
    if (element.isSupported) {
      this.qcClient.uploadQc(element, albumId);
    }

    // Wrap the logo in a href to the new album
    const $image = $base.find('img');
    $image.wrap(`<a href='${AlbumLink}' target='_blank' title='Go to album'></a>`);
    $image.removeAttr('title');

    // Remove processing
    $processing.remove();

    // Update the marker
    const $qcMarker = $base.find('.qc-marker:not(:first-child)').first();
    $qcMarker.attr('title', 'You have uploaded your QC')
      .css('cursor', 'help')
      .css('color', 'green')
      .text('✓');

    // Remove the click handler
    $base.off();

    // Show it again
    $base.show();
  }
}

// Inject snackbar css style
GM_addStyle(GM_getResourceText('sweetalert2'));

// Setup proper settings menu
GM_config.init('Settings', {
  serverSection: {
    label: 'QC Server settings',
    type: 'section',
  },
  swaggerDocUrl: {
    label: 'Swagger documentation URL',
    type: 'text',
    default: 'https://www.qc-server.cf/api/doc.json',
  },
  uploadSection: {
    label: 'Upload API Options',
    type: 'section',
  },
  imgurApi: {
    label: 'Select your Imgur API',
    type: 'radio',
    default: 'imgur',
    options: {
      imgur: 'Imgur API (Free)',
      rapidApi: 'RapidAPI (Freemium)',
    },
  },
  imgurSection: {
    label: 'Imgur Options',
    type: 'section',
  },
  imgurApiHost: {
    label: 'Imgur host',
    type: 'text',
    default: 'api.imgur.com',
  },
  imgurClientId: {
    label: 'Imgur Client-ID',
    type: 'text',
    default: 'e4e18b5ab582b4c',
  },
  rapidApiSection: {
    label: 'RadidAPI Options',
    type: 'section',
  },
  rapidApiHost: {
    label: 'RapidAPI host',
    type: 'text',
    default: 'imgur-apiv3.p.rapidapi.com',
  },
  rapidApiKey: {
    label: 'RapidAPI key (only needed if RapidApi select above)',
    type: 'text',
    default: '',
  },
  rapidApiBearer: {
    label: 'RapidAPI access token (only needed if RapidApi select above)',
    type: 'text',
    default: '',
  },
});

// Reload page if config changed
GM_config.onclose = (saveFlag) => {
  if (saveFlag) {
    window.location.reload();
  }
};

// Register menu within GM
GM_registerMenuCommand('Settings', GM_config.open);

// eslint-disable-next-line func-names
(async function () {
  // Setup the logger.
  Logger.useDefaults();

  // Log the start of the script.
  Logger.info(`Starting extension, version ${GM_info.script.version}`);

  // Get the username
  const username = $('#dropdownMenu1').text();
  if (typeof username === 'undefined' || username == null || username === '') {
    Snackbar('You need to be logged in to use this extension.');

    return;
  }

  /** @type {SwaggerClient} */
  let client;

  // Try to create Swagger client from our own documentation
  try {
    client = await new SwaggerClient({ url: GM_config.get('swaggerDocUrl') });
  } catch (error) {
    Snackbar('We are unable to connect to FR:Reborn, features will be disabled.');
    Logger.error(`We are unable to connect to FR:Reborn: ${GM_config.get('swaggerDocUrl')}`, error);

    return;
  }

  // Create QC and Imgur clients, use hash of username
  const qcClient = new QC(GM_info.script.version, client, SparkMD5.hash(username));
  const imgurClient = new Imgur(GM_info.script.version, GM_config);

  // Create the processor which does the heavy lifting
  const processor = new Processor(qcClient, imgurClient);

  // Create a simple UploadHandler
  const uploadHandler = async function () {
    const $this = $(this);
    const itemUrl = $this.data('item-url');
    const element = new Element($this);

    // Go to the QC pictures URL and grab all image src's
    $.get(itemUrl, (data) => {
      if (data.indexOf('long time no operation ,please sign in again') !== -1) {
        Logger.info('No longer logged in, reloading page for user...');
        window.location.reload();

        return;
      }

      const imageUrls = [];
      $('<div/>').html(data).find('div.container.container-top60 > img').each(function () {
        imageUrls.push($(this).attr('src'));
      });

      element.images = imageUrls;

      processor.UploadToImgur(element);
    });
  };

  $('.myparcels-ul').find('span.glyphicon.glyphicon-picture').each(async function () {
    const $this = $(this);
    const element = new Element($this);

    // This plugin only works for certain websites, so check if element is supported
    if (!element.isSupported) {
      const $upload = $('<ul><li><span style="cursor: pointer;margin-left:-29px;"><img src="https://s.imgur.com/images/favicon-16x16.png" alt="Create a basic album"></span></li></ul>');
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="Not a supported URL, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">✖</span>'));
      $upload.data('item-url', $(this).parent().attr('href'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);

      return;
    }

    const $loading = $('<ul><li><span style="cursor: wait;margin-left:-29px;"><img src="https://i.imgur.com/lnFQTQz.gif" alt="Loading..."></span></li></ul>');
    $this.parents('td').first().append($loading);

    // Define upload object
    const $upload = $('<ul><li><span class="qc-marker" style="cursor: pointer;margin-left:-29px;"><img src="https://s.imgur.com/images/favicon-16x16.png" alt="Upload your QC"></span></li></ul>');
    $upload.data('item-url', $(this).parent().attr('href'));

    // If we couldn't talk to FR:Reborn, assume everything is dead and use the basic uploader.
    const albumId = await qcClient.existingAlbumByOrderId(element);
    if (albumId === '-1') {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="FR:Reborn returned an error, but you can still create an album. The QC\'s are not stored and you\'ll have to create a new album if you lose the link.">⚠️</span>'));
      $upload.data('item-url', $(this).parent().attr('href'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // Has anyone ever uploaded a QC, if not, show a red marker
    const exists = await qcClient.exists(element.url);
    if (!exists) {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:red;font-weight: bold;" title="No QC in database, please upload.">(!)</span>'));
      $upload.on('click', uploadHandler);

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // Have you ever uploaded a QC? If so, link to that album
    const $image = $upload.find('img');
    if (albumId !== null && albumId !== '-1') {
      $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:green;font-weight: bold;" title="You have uploaded a QC">✓</span>'));
      $image.wrap(`<a href='https://imgur.com/a/${albumId}' target='_blank' title='Go to album'></a>`);
      $image.removeAttr('title');

      $this.parents('td').first().append($upload);
      $loading.remove();

      return;
    }

    // A previous QC exists, but you haven't uploaded yours yet, show orange marker
    $upload.find('span').first().after($('<span class="qc-marker" style="cursor:help;margin-left:5px;color:orange;font-weight: bold;" title="Your QC is not yet in the database, please upload.">(!)</span>'));
    $upload.on('click', uploadHandler);

    $this.parents('td').first().append($upload);
    $loading.remove();
  });
}());