SB100 / PTP TMDb Movie Recommendations

// ==UserScript==
// @namespace    https://openuserjs.org/users/SB100
// @name         PTP TMDb Movie Recommendations
// @description  Show recommendations from TMDb on the PTP movie page
// @updateURL    https://openuserjs.org/meta/SB100/PTP_TMDb_Movie_Recommendations.meta.js
// @version      1.2.0
// @author       SB100
// @copyright    2021, SB100 (https://openuserjs.org/users/SB100)
// @license      MIT
// @match        https://passthepopcorn.me/torrents.php?id=*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// ==/UserScript==

// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==

/* jshint esversion: 6 */

/**
 * =============================
 * ADVANCED OPTIONS
 * =============================
 */

// Select whether to use Similar or Recommendations algorithm from TMDb.
// More info found here: https://www.themoviedb.org/talk/55635e59c3a368542c0026e6
const SETTING_USE_SIMILAR_OR_RECOMMENDATIONS = 'similar'; // 'similar' OR 'recommendations'

/**
 * =============================
 * END ADVANCED OPTIONS
 * DO NOT MODIFY BELOW THIS LINE
 * =============================
 */

const BASE_TMDB_URL = 'https://api.themoviedb.org/3';

/**
 * Gets your TMDb API key. Asks the user for one if it hasn't been set yet
 */
function getTmdbApiKey() {
  const key = GM_getValue('tmdb_key', '');
  if (!key) {
    const input = window.prompt(`Please input your TMDb API key.
If you don't have one, please signup for one at https://themoviedb.org.
Then go to Settings -> API -> API Key (v3 auth)

Disable this userscript until you have one as this prompt will continue to show until one is provided`);
    const trimmed = input && input.trim();

    if (/[a-f0-9]{32}/.test(trimmed)) {
      GM_setValue('tmdb_key', trimmed);
      return trimmed;
    }
  }

  return key;
}

/**
 * Turn a title into a url safe string
 */
function encodeTitle(title) {
  return encodeURIComponent(title.replace(/[^\w\d\s\-\.]+/gi, ''));
}

/**
 * Query the API for some results
 */
function queryApi(path, params = {}) {
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });

  const paramStr = new URLSearchParams(params).toString();
  const url = `${BASE_TMDB_URL}${path}${paramStr.length > 0 ? `?${paramStr}` : ''}`

  GM_xmlhttpRequest({
    method: 'get',
    url: url,
    timeout: 10000,
    onloadstart: () => {},
    onload: (result) => {
      if (result.status === 401) {
        const resp = JSON.parse(result.response);
        if (resp.status_code === 7) {
          GM_deleteValue('tmdb_key');
          alert(`Invalid TMDb API key. It has now been removed.
Please refresh the page and input a valid TMDb API key to continue using this userscript.`);
          rejecter(new Error('Invalid TMDb API Key'));
          return;
        }
      }

      if (result.status !== 200) {
        console.log('[TMDb Movie Recommendations]', result);
        rejecter(new Error('Not OK'));
        return;
      }

      resolver(JSON.parse(result.response))
    },
    onerror: (result) => {
      rejecter(result)
    },
    ontimeout: (result) => {
      rejecter(result)
    }
  });

  return p;
}

/**
 * Find the movies IMDb ID from the page
 */
function findImdbIdFromPage() {
  const elem = document.getElementById('imdb-title-link');
  const href = elem && elem.href;
  return href && href.match(/tt\d+/)[0];
}

/**
 * Find the TMBd movie info for the IMDb movie passed in
 */
async function findMovieInfoByIMDb(imdbId, key) {
  const movieInfo = await queryApi(`/find/${imdbId}`, {
    external_source: 'imdb_id',
    api_key: key
  });
  if (movieInfo && Array.isArray(movieInfo.movie_results) && movieInfo.movie_results.length > 0) {
    return movieInfo.movie_results[0];
  }

  return null;
}

/**
 * Get movie recommendations for a TMDb movie entry
 */
async function getSuggestions(tmdbId, key, type) {
  const movieRecommendations = await queryApi(`/movie/${tmdbId}/${type}`, {
    api_key: key
  });
  if (movieRecommendations && Array.isArray(movieRecommendations.results) && movieRecommendations.results.length > 0) {
    return movieRecommendations
      .results
      .filter(r => typeof r.release_date !== 'undefined')
      .sort((a, b) => {
        if (a.vote_average > b.vote_average) return -1;
        if (b.vote_average > a.vote_average) return 1;
        return 0;
      });
  }

  return null;
}

/**
 * Turn the results from the API into something the user can see in the sidebar
 */
function buildResults(results) {
  const list = document.createElement('ul');
  list.classList.add('list', 'list--unstyled');
  results.forEach(result => {
    const year = result.release_date.split('-')[0];
    const li = document.createElement('li');
    li.innerHTML = `<span style="font-size: 0.75em; vertical-align: middle;">(${result.vote_average})</span>
<a href="/torrents.php?order_by=relevance&searchstr=${encodeTitle(`${result.title} ${year}`)}" style="vertical-align: middle;">${result.title}</a>
<span style="vertical-align: middle;">[${year}]</span>`;
    li.title = `Popularity: ${result.popularity}; Vote count: ${result.vote_count}; Vote average: ${result.vote_average}`;

    list.appendChild(li);
  });

  const panel = document.createElement('div');
  panel.classList.add('panel');
  panel.innerHTML = `
    <div class='panel__heading'>
        <span class="panel__heading__title">TMDb ${SETTING_USE_SIMILAR_OR_RECOMMENDATIONS === 'similar' ? 'similar movies' : 'recommendations'}</span>
        <span style="float:right; font-size: 0.9em"></span>
    </div>
    <div class='panel__body'>${list.outerHTML}</div>
`;

  const sidebar = document.querySelector('.sidebar');
  sidebar && sidebar.appendChild(panel);
}

/**
 * Main script runner
 */
(async function () {
  'use strict';

  if (['similar', 'recommendations'].includes(SETTING_USE_SIMILAR_OR_RECOMMENDATIONS) === false) {
    alert('[TMDb Movie Recommendations] Configuration error. Allowed values are "similar" and "recommendations"');
    return;
  }

  const key = getTmdbApiKey();
  if (!key) {
    console.log(`[TMDb Movie Recommendations] No valid API key found, exiting userscript`);
    return;
  }

  const imdb = findImdbIdFromPage();
  if (!imdb) {
    console.log(`[TMDb Movie Recommendations] Couldn't find IMDb ID`);
    return;
  }

  const movieInfo = await findMovieInfoByIMDb(imdb, key);
  if (!movieInfo) {
    console.log(`[TMDb Movie Recommendations] No movie info found from API`);
    return;
  }

  const recommendations = await getSuggestions(movieInfo.id, key, SETTING_USE_SIMILAR_OR_RECOMMENDATIONS);
  if (!recommendations) {
    console.log(`[TMDb Movie Recommendations] No recommendations found from API`);
    return;
  }

  buildResults(recommendations);
})();