SB100 / BTN Sonarr Integration

// ==UserScript==
// @name         BTN Sonarr Integration
// @namespace    https://openuserjs.org/users/SB100
// @description  The BTN <-> Sonarr Integration we always wanted
// @updateURL    https://openuserjs.org/meta/SB100/BTN_Sonarr_Integration.meta.js
// @version      1.0.1
// @author       SB100
// @copyright    2025, SB100 (https://openuserjs.org/users/SB100)
// @license      MIT
// @match        https://broadcasthe.net/series.php?id=*
// @grant        GM.xmlHttpRequest
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.registerMenuCommand
// @connect      thetvdb.com

// ==/UserScript==

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

/* jshint esversion: 11 */

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

// show debug logs in the browser console
const SETTING_DEBUG = false;

// how long to cache sonarr existing series for
// default = 1000 * 60 * 10 = 10 minutes
const SETTING_CACHE_TIME = 1000 * 60 * 10;

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

// ================================= Basics

/**
 * Try parsing a string into JSON, otherwise fallback
 */
function JsonParseWithDefault(s, fallback = null) {
  try {
    return JSON.parse(s);
  }
  catch (e) {
    return fallback;
  }
}

/**
 * Print a debug message, if enabled
 */
function debug(strOrStrArray) {
  if (!SETTING_DEBUG) return;
  // eslint-disable-next-line no-console
  console.log(
    `[BTN Sonarr Integration] ${
      Array.isArray(strOrStrArray) ? strOrStrArray.join(' - ') : strOrStrArray
    }`
  );
}

// ================================= Config

/**
 * Get a config value from the GM cache
 */
async function getConfig(key, fallback = '') {
  return GM.getValue(key, fallback);
}

/**
 * Set a config value into the GM cache
 */
async function setConfig(key, value) {
  await GM.setValue(key, value);
}

/**
 * Get all settings stored in localStorage for this script
 */
function getSettings() {
  const settings = window.localStorage.getItem('sonarrIntegrationSettings');
  // eslint-disable-next-line no-use-before-define
  return JsonParseWithDefault(settings || {}, {});
}

/**
 * Set a setting into localStorage for this script
 */
function setSetting(name, value) {
  const json = getSettings();
  json[name] = value;
  window.localStorage.setItem(
    'sonarrIntegrationSettings',
    JSON.stringify(json)
  );
}

/**
 * Set a sonarr bar setting into the settings localStorage object
 */
function setSonarrBarSetting(type, value) {
  const existingSettings = getSettings().sonarrBar || {};
  const newSettings = {
    ...existingSettings,
    [type]: value
  };
  setSetting('sonarrBar', newSettings);
}

// ================================= Query

/**
 * Query the sonarr api
 */
async function query(url, method = 'get', params = {}, sonarrApiKey = null) {
  let resolver;
  let rejecter;
  const p = new Promise((resolveFn, rejectFn) => {
    resolver = resolveFn;
    rejecter = rejectFn;
  });

  const clonedUrl = new URL(url);

  const obj = {
    method,
    timeout: 60000,
    onloadstart: () => {},
    onload: (response) => resolver(response),
    onerror: (response) => rejecter(response),
    ontimeout: (response) => rejecter(response),
  };

  if (method === 'post') {
    const headers = {
      'Content-Type': 'application/json',
    };

    if (sonarrApiKey) {
      clonedUrl.search = new URLSearchParams({
        apikey: sonarrApiKey,
      }).toString();
    }

    const final = Object.assign(obj, {
      url: clonedUrl.toString(),
      headers,
      data: JSON.stringify(params),
    });

    GM.xmlHttpRequest(final);
  }
  else {
    const newParams = sonarrApiKey ?
      {
        ...params,
        apikey: sonarrApiKey
      } :
      params;
    clonedUrl.search = new URLSearchParams(newParams).toString();

    const final = Object.assign(obj, {
      url: clonedUrl.toString(),
    });

    GM.xmlHttpRequest(final);
  }

  return p;
}

/**
 * Get request to Sonarr API. Parse results
 */
async function sonarrGet(path, params = {}) {
  const sonarrUrl = (await getConfig('host')).replace(/\/$/, '');
  const apiKey = await getConfig('key');
  const url = `${sonarrUrl}/api/v3`;

  return query(new URL(`${url}${path}`), 'get', params, apiKey).then(
    (response) => JSON.parse(response.responseText)
  );
}

/**
 * Post request to Sonarr API. Parse results
 */
async function sonarrPost(path, params = {}) {
  const sonarrUrl = (await getConfig('host')).replace(/\/$/, '');
  const apiKey = await getConfig('key');
  const url = `${sonarrUrl}/api/v3`;

  return query(new URL(`${url}${path}`), 'post', params, apiKey).then(
    (response) => JSON.parse(response.responseText)
  );
}

/**
 * Either parse the tvdb id from the url, or query the page for it
 */
async function tvdbGetIdFromUrl(tvdbUrl) {
  const url = new URL(tvdbUrl);

  // for format: https://thetvdb.com/?tab=series&id=
  if (url.searchParams.get('id') !== null) {
    return url.searchParams.get('id');
  }

  // for format: https://www.thetvdb.com/series
  return query(url).then((response) => {
    const parser = new DOMParser();
    const html = parser.parseFromString(response.responseText, 'text/html');
    return (
      html.querySelector('#series_basic_info li span')?.textContent ?? null
    );
  });
}

/**
 * Get existing tvdb ids from sonarr, and cache for the appropriate time
 */
async function getExistingSonarrTvdbIds() {
  try {
    const {
      sonarrTvdbIds,
      lastUpdated
    } = getSettings();

    if (
      !lastUpdated ||
      new Date().getTime() > lastUpdated + SETTING_CACHE_TIME
    ) {
      const hasSuccessfulConnection = await getConfig(
        'hasSuccessfulConnection',
        false
      );
      if (!hasSuccessfulConnection) {
        return [];
      }

      const allSeries = await sonarrGet('/series');
      const tvdbIds = allSeries.reduce((result, series) => {
        // eslint-disable-next-line no-param-reassign
        result[series.tvdbId] = series.titleSlug;
        return result;
      }, {});

      setSetting('lastUpdated', new Date().getTime());
      setSetting('sonarrTvdbIds', tvdbIds);

      return tvdbIds;
    }

    return sonarrTvdbIds;
  }
  catch (e) {
    await setConfig('hasSuccessfulConnection', false);
    // TODO update for sonarr
    // const sonarrBar = document.querySelector('.btn-sonarr__bar');
    // sonarrBar.dataset.isLoaded = '0';
    // debug([`Couldn't get existing IMDb IDs from Radarr`, e.message]);
    return [];
  }
}

/**
 * Get the data needed to populate the sonarr bar
 */
async function getDataForSonarrBar() {
  try {
    const [rootFolders, qualityProfiles, tags] = await Promise.all([
      sonarrGet('/rootfolder'),
      sonarrGet('/qualityprofile'),
      sonarrGet('/tag'),
    ]);

    return {
      rootFolders,
      qualityProfiles,
      tags,
    };
  }
  catch (e) {
    debug([`Couldn't connect to Sonarr`, e.message]);
    return {
      error: true,
    };
  }
}

// ================================= Helpers

/**
 * Tries to get the theme the user is using
 */
function getTheme() {
  const linkTags = Array.from(
    document.querySelectorAll('link[rel="stylesheet"]')
  );

  for (let i = 0, len = linkTags.length; i < len; i += 1) {
    const tag = linkTags[i];
    if (tag.href.includes('btn-future.css')) {
      return 'btn-future';
    }
  }

  return 'default';
}

/**
 * Get the BTN series id from the url
 */
function getBtnSeriesIdFromUrl() {
  if (window.location.pathname !== '/series.php') {
    debug(
      `Could not find BTN series ID from URL: "${window.location.toString()}"`
    );
    return null;
  }

  const urlParams = new URLSearchParams(window.location.search);
  return urlParams.get('id') ?? null;
}

/**
 * Get a tvdb id from the BTN id that has been saved in settings
 */
function getTvdbIdFromBtnId(btnId) {
  const {
    btnToTvdbMap = {}
  } = getSettings();
  return btnToTvdbMap[btnId];
}

/**
 * Save a BTN -> tvdb Id mapping
 */
function setTvdbIdForBtnId(btnId, tvdbId) {
  const {
    btnToTvdbMap = {}
  } = getSettings();
  btnToTvdbMap[btnId] = tvdbId;
  setSetting('btnToTvdbMap', btnToTvdbMap);
}

/**
 * Get the tvdb url from the BTN series page
 */
function getTvdbUrlFromBtnSeriesPage() {
  if (window.location.pathname !== '/series.php') {
    debug(`Could not find tvdb URL from URL: "${window.location.toString()}"`);
    return null;
  }

  const a = document.querySelector(
    '[href*="thetvdb.com/series"], [href*="thetvdb.com/?tab=series&id="]'
  );
  return a?.href ?? null;
}

/**
 * All encompassing function to find the tvdb id from a BTN series page
 */
async function getTvdbId() {
  const btnId = getBtnSeriesIdFromUrl();
  const maybeLocalTvdbId = getTvdbIdFromBtnId(btnId);
  if (maybeLocalTvdbId) {
    debug(`tvdb ID exists locally [BTN: ${btnId}] [tvdb: ${maybeLocalTvdbId}]`);
    return maybeLocalTvdbId;
  }

  const tvdbUrl = getTvdbUrlFromBtnSeriesPage();
  if (!tvdbUrl) {
    debug(
      `Could not obtain tvdb URL from BTN series page: "${window.location.toString()}"`
    );
    return null;
  }

  const maybeRemoteTvdbId = await tvdbGetIdFromUrl(tvdbUrl);
  const asInt = Number.parseInt(maybeRemoteTvdbId, 10);
  if (Number.isNaN(asInt)) {
    debug(
      `Could not obtain tvdb ID from remote for URL: "${window.location.toString()}"`
    );
    return null;
  }

  debug(`tvdb ID obtained remotely [BTN: ${btnId}] [tvdb: ${asInt}]`);
  setTvdbIdForBtnId(btnId, asInt);
  return asInt;
}

// ================================= Event Handlers

/**
 * Force Sonarr Bar to show if the state of a checkbox is changed
 */
function handleCheckboxChange() {
  const sonarrBar = document.querySelector('.btn-sonarr__bar');

  const allCheckboxes = Array.from(
    document.querySelectorAll('.btn-sonarr__checkbox')
  );

  if (allCheckboxes.some((checkbox) => checkbox.checked)) {
    if (!sonarrBar.classList.contains('btn-sonarr__bar--showing')) {
      sonarrBar.classList.add('btn-sonarr__bar--showing');
    }

    // if it hasn't been already, add dropdowns to the sonarr bar
    // eslint-disable-next-line no-use-before-define
    populateSonarrBar(sonarrBar);
  }
  else if (sonarrBar.classList.contains('btn-sonarr__bar--showing')) {
    sonarrBar.classList.remove('btn-sonarr__bar--showing');
  }
}

/**
 * Close the Multiselect options if we have it open, and we click outside of it on the sonarr bar
 */
function handleSonarrBarClick(event) {
  const multiSelectOptions = document.querySelector('.multi-select__options');

  // if options is opened, and we click outside the multi-select, close options
  if (
    multiSelectOptions &&
    multiSelectOptions.classList.contains('multi-select__options--opened')
  ) {
    const targetSelector = '.multi-select';
    let {
      target
    } = event;

    while (target && target.matches('.btn-sonarr__bar') === false) {
      if (target.matches(targetSelector)) {
        return;
      }

      target = target.parentNode;
    }

    multiSelectOptions.classList.remove('multi-select__options--opened');
  }
}

/**
 * Add the series to sonarr with the selected options
 */
async function handleSonarrBarSubmit(event) {
  event.preventDefault();

  const addSeriesButton = document.getElementById('btn-sonarr__bar-submit');

  // if errored, and form is submitted, reset the form
  if (event.target.dataset.reset === '1') {
    // eslint-disable-next-line no-param-reassign
    event.target.dataset.reset = '0';
    addSeriesButton.value = 'Add Series';
    return;
  }

  // disable the button
  addSeriesButton.value = 'Processing …';
  addSeriesButton.disabled = true;
  // eslint-disable-next-line no-param-reassign
  event.target.dataset.reset = '1';

  // get the form data
  const sonarrBarFormData = Object.fromEntries(
    Array.from(new FormData(event.target))
  );

  try {
    const tvdbId = parseInt(
      document.querySelector('.btn-sonarr__checkbox:checked')?.value,
      10
    );
    if (Number.isNaN(tvdbId)) {
      addSeriesButton.disabled = false;
      addSeriesButton.value = 'Nothing selected';
      return;
    }

    const tagIds = Array.from(
      document.querySelectorAll('.multi-select__option-checkbox:checked')
    ).map((c) => parseInt(c.value, 10));

    const seriesInfos = await sonarrGet('/series/lookup', {
      term: `tvdb:${tvdbId}`,
    });
    if (!Array.isArray(seriesInfos) || seriesInfos.length !== 1) {
      addSeriesButton.disabled = false;
      addSeriesButton.value = 'Invalid TVDb info';
      return;
    }

    const addBody = {
      ...seriesInfos[0],
      alternateTitles: [],
      addOptions: {
        ignoreEpisodesWithFiles: false,
        ignoreEpisodesWithoutFiles: false,
        searchForCutoffUnmetEpisodes: sonarrBarFormData.searchCutoffUnmetEpisodes === 'on',
        searchForMissingEpisodes: sonarrBarFormData.searchMissingEpisodes === 'on',
        monitor: sonarrBarFormData.monitor,
      },
      monitored: sonarrBarFormData.monitor !== 'none',
      tags: tagIds,
      rootFolderPath: sonarrBarFormData.rootFolderPath,
      qualityProfileId: parseInt(sonarrBarFormData.qualityProfileId, 10),
      seasonFolder: sonarrBarFormData.seasonFolder === 'on',
      path: `${sonarrBarFormData.rootFolderPath}/${seriesInfos[0].folder}`,
      added: new Date().toISOString(),
    };
    delete addBody.folder;
    delete addBody.remotePoster;

    addSeriesButton.value = 'Adding …';

    const addResult = await sonarrPost('/series', addBody);
    if (addResult.errors) {
      addSeriesButton.value = 'Error adding series';
      debug(['Error importing series', addResult.errors.$]);
      setSetting('lastUpdated', 0);
      setSetting('sonarrTvdbIds', {});
      return;
    }

    addSeriesButton.value = 'Added!';

    // update cache
    const {
      sonarrTvdbIds
    } = getSettings();
    setSetting('sonarrTvdbIds', {
      ...sonarrTvdbIds,
      [addResult.tvdbId]: addResult.titleSlug,
    });

    // refresh the checkboxes to show the new ribbons
    // eslint-disable-next-line no-use-before-define
    await addCheckboxesToSeries();
  }
  catch (e) {
    await setConfig('hasSuccessfulConnection', false);
    const sonarrBar = document.querySelector('.btn-sonarr__bar');
    sonarrBar.dataset.isLoaded = '0';
    sonarrBar.innerHTML = `<div class="loading-icon__cont">Error adding to Sonarr. <a href="#sonarr">Recheck your connection</a> or check your browser console for more info</div>`;
    debug([`Couldn't import series`, e.message]);
    return;
  }

  addSeriesButton.disabled = false;
}

// ================================= UI Sonarr Bar

/**
 * Create a loading icon
 */
function createLoadingIcon() {
  const loader = document.createElement('div');
  loader.className = 'loading-icon';

  const container = document.createElement('div');
  container.className = 'loading-icon__cont';
  container.appendChild(loader);

  return container;
}

/**
 * Grab all contents needed to populate the sonarr bar, and render it
 */
async function populateSonarrBar(sonarrBar) {
  // make sure we've successfully connected before
  const hasSuccessfulConnection = await getConfig(
    'hasSuccessfulConnection',
    false
  );
  if (!hasSuccessfulConnection) {
    // eslint-disable-next-line no-param-reassign
    sonarrBar.innerHTML = `<div class="loading-icon__cont"><a href="#sonarr">Configure and Test</a> your Sonarr Connection first</div>`;
    return;
  }

  // no need to query sonarr again if we've fully loaded the options before
  if (sonarrBar.dataset.isLoaded === '1') {
    return;
  }

  // query sonarr for data
  const {
    qualityProfiles,
    rootFolders,
    tags,
    error
  } =
  await getDataForSonarrBar();

  if (error) {
    await setConfig('hasSuccessfulConnection', false);
    // eslint-disable-next-line no-param-reassign
    sonarrBar.dataset.isLoaded = '0';
    // eslint-disable-next-line no-param-reassign
    sonarrBar.innerHTML = `<div class="loading-icon__cont">Error loading Sonarr settings. <a href="#sonarr">Recheck your connection</a> or check the browser console for more info</div>`;
    return;
  }

  // get saved settings for the sonarr bar
  const settings = getSettings().sonarrBar || {};

  // build sonarr bar inners
  const qualityProfileOptions = qualityProfiles
    .map(
      (qp) =>
      `<option value="${qp.id}" ${
          settings.qualityProfileId === qp.id ? 'selected' : ''
        }>${qp.name}</option>`
    )
    .join('');

  const rootFolderOptions = rootFolders
    .map(
      (r) =>
      `<option value="${r.path}" ${
          settings.rootFolderPath === r.path ? 'selected' : ''
        }>${r.path}</option>`
    )
    .join('');

  let tagsSelected = 0;
  const tagOptions = tags
    .map((tag) => {
      let checked = '';
      if ((settings.tags || []).includes(tag.id)) {
        checked = 'checked';
        tagsSelected += 1;
      }
      return `<label for="multi-select__option-${tag.id}" class="multi-select__option">
        <input type="checkbox" class="multi-select__option-checkbox" id="multi-select__option-${tag.id}" value="${tag.id}" ${checked}> 
        <span class="multi-select__option-text">${tag.label}</span>
      </label>`;
    })
    .join('');

  // eslint-disable-next-line no-param-reassign
  sonarrBar.innerHTML = `
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-root-folder">
    <span class="btn-sonarr__bar-label-text">Root Folder</span>
    <select class="btn-sonarr__bar-select" id="btn-sonarr__bar-root-folder" name="rootFolderPath">
      ${rootFolderOptions}
    </select>
  </label>
  
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-monitor">
    <span class="btn-sonarr__bar-label-text">Monitor</span>
    <select class="btn-sonarr__bar-select" id="btn-sonarr__bar-monitor" name="monitor">
      <option value="all" ${
        settings.monitor === 'all' ? 'selected' : ''
      }>All Episodes</option>
      <option value="future" ${
        settings.monitor === 'future' ? 'selected' : ''
      }>Future Episodes</option>
      <option value="missing" ${
        settings.monitor === 'missing' ? 'selected' : ''
      }>Missing Episodes</option>
      <option value="existing" ${
        settings.monitor === 'existing' ? 'selected' : ''
      }>Existing Episodes</option>
      <option value="recent" ${
        settings.monitor === 'recent' ? 'selected' : ''
      }>Recent Episodes</option>
      <option value="pilot" ${
        settings.monitor === 'pilot' ? 'selected' : ''
      }>Pilot Episodes</option>
      <option value="firstSeason" ${
        settings.monitor === 'firstSeason' ? 'selected' : ''
      }>First Season</option>
      <option value="lastSeason" ${
        settings.monitor === 'lastSeason' ? 'selected' : ''
      }>Last Season</option>
      <option value="monitorSpecials" ${
        settings.monitor === 'monitorSpecials' ? 'selected' : ''
      }>Monitor Specials</option>
      <option value="unmonitorSpecials" ${
        settings.monitor === 'unmonitorSpecials' ? 'selected' : ''
      }>Unmonitor Specials</option>
      <option value="none" ${
        settings.monitor === 'none' ? 'selected' : ''
      }>None</option>
    </select>
  </label>
  
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-qp">
    <span class="btn-sonarr__bar-label-text">Quality Profile</span>
    <select class="btn-sonarr__bar-select" id="btn-sonarr__bar-qp" name="qualityProfileId">
      ${qualityProfileOptions}
    </select>
  </label>
  
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-series-type">
    <span class="btn-sonarr__bar-label-text">Series Type</span>
    <select class="btn-sonarr__bar-select" id="btn-sonarr__bar-series-type" name="seriesType">
      <option value="standard" ${
        settings.seriesType === 'standard' ? 'selected' : ''
      }>Standard</option>
      <option value="daily" ${
        settings.seriesType === 'daily' ? 'selected' : ''
      }>Daily / Date</option>
      <option value="anime" ${
        settings.seriesType === 'anime' ? 'selected' : ''
      }>Anime / Absolute</option>
    </select>
  </label>
  
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-tags">
    <span class="btn-sonarr__bar-label-text">Tags</span>
    <div class="multi-select">
      <div class="multi-select__options">
        ${tagOptions}
      </div>
      <button class="multi-select__button">
        <span class="multi-select__button-icon">☰</span>
        <span class="multi-select__button-text">${tagsSelected} Tag(s)</span>
      </button>
    </div>
  </label>
  
  <span class="btn-sonarr__bar-label">
    <span class="btn-sonarr__bar-label-text">Create</span>
    <span class="btn-sonarr__bar-multi-label">
      <label for="btn-sonarr__bar-season-folder">
        <input type="checkbox" class="btn-sonarr__bar-checkbox" id="btn-sonarr__bar-season-folder" name="seasonFolder" ${
          settings.seasonFolder === true ? 'checked' : ''
        }>
        Season folder
      </label>
    </span>
  </span>
  
  <span class="btn-sonarr__bar-label">
    <span class="btn-sonarr__bar-label-text">Search for</span>
    <span class="btn-sonarr__bar-multi-label">
      <label for="btn-sonarr__bar-search-missing-episodes">
        <input type="checkbox" class="btn-sonarr__bar-checkbox" id="btn-sonarr__bar-search-missing-episodes" name="searchMissingEpisodes" ${
          settings.searchMissingEpisodes === true ? 'checked' : ''
        }>
        Missing
      </label>
      
      <label for="btn-sonarr__bar-search-cutoff-unmet-episodes">
        <input type="checkbox" class="btn-sonarr__bar-checkbox" id="btn-sonarr__bar-search-cutoff-unmet-episodes" name="searchCutoffUnmetEpisodes" ${
          settings.searchCutoffUnmetEpisodes === true ? 'checked' : ''
        }>
        Cutoff unmet
      </label>
    </span>
  </span>
  
  <label class="btn-sonarr__bar-label" for="btn-sonarr__bar-submit">
    <span class="btn-sonarr__bar-label-text">Add Series</span>
    <input type="submit" class="btn-sonarr__bar-button" id="btn-sonarr__bar-submit" value="Add Series" />
  </label>
  `;

  // multi select button
  // open and close menu
  document
    .querySelector('.multi-select__button')
    .addEventListener('click', (event) => {
      event.preventDefault();
      const target = event.target.matches('.multi-select__button') ?
        event.target :
        event.target.parentNode;
      const options = target.previousElementSibling;
      if (options.classList.contains('multi-select__options--opened')) {
        options.classList.remove('multi-select__options--opened');
      }
      else {
        options.classList.add('multi-select__options--opened');
      }
    });

  // multi select checkboxes changed
  Array.from(
    document.querySelectorAll('.multi-select__option-checkbox')
  ).forEach((checkbox) => {
    checkbox.addEventListener('change', () => {
      const selected = Array.from(
        document.querySelectorAll('.multi-select__option-checkbox:checked')
      );

      // updated how many tags have been selected
      document.querySelector(
        '.multi-select__button-text'
      ).innerHTML = `${selected.length} Tag(s)`;

      // save new settings
      setSonarrBarSetting(
        'tags',
        selected.map((s) => parseInt(s.value, 10))
      );
    });
  });

  // on select change, save the setting to use for next time
  Array.from(document.querySelectorAll('.btn-sonarr__bar-select')).forEach(
    (select) => {
      select.addEventListener('change', (event) => {
        const parsed = parseInt(event.target.value, 10);
        setSonarrBarSetting(
          select.getAttribute('name'),
          Number.isNaN(parsed) ? event.target.value : parsed
        );
      });
    }
  );

  // on checkbox change, save the setting to use for next time
  Array.from(document.querySelectorAll('.btn-sonarr__bar-checkbox')).forEach(
    (checkbox) => {
      checkbox.addEventListener('change', (event) => {
        const {
          checked
        } = event.target;
        setSonarrBarSetting(checkbox.getAttribute('name'), checked);
      });
    }
  );

  // eslint-disable-next-line no-param-reassign
  sonarrBar.dataset.isLoaded = '1';
}

/**
 * Add the sonarr bar to the page
 */
async function addSonarrBar() {
  const alwaysShow = await getConfig('alwaysShow', false);
  const sonarrBar = document.createElement('form');
  sonarrBar.className = `btn-sonarr__bar`;
  sonarrBar.method = 'post';
  sonarrBar.addEventListener('submit', handleSonarrBarSubmit);
  sonarrBar.addEventListener('click', handleSonarrBarClick);
  sonarrBar.appendChild(createLoadingIcon());

  const wrapper = document.getElementById('wrapper');
  wrapper.appendChild(sonarrBar);

  if (alwaysShow) {
    sonarrBar.classList.add('btn-sonarr__bar--showing');
    populateSonarrBar(sonarrBar);
  }
}

// ================================= UI Checkboxes

/**
 * Add checkboxes to the series posters
 */
async function addCheckboxesToSeries() {
  // remove existing checkboxes and ribbons
  Array.from(
    document.querySelectorAll('.btn-sonarr__checkbox, .btn-sonarr__ribbon')
  ).forEach((elem) => elem.remove());

  // get setting on whether we always want to show ribbons or not
  const ribbons = await getConfig('ribbons', false);
  // get sonarr url
  const sonarrUrl = (await getConfig('host')).replace(/\/$/, '');

  // find img poster
  const imgElem = document.querySelector('.sidebar img[onload]');
  if (!imgElem) {
    debug(`Could not find cover poster for "${window.location.toString()}"`);
    return false;
  }

  const parent = imgElem.parentNode;
  parent.style.position = 'relative';

  // add a loader whilst we do the api calls
  const loader = createLoadingIcon();
  loader.style.position = 'absolute';
  loader.style.inset = 0;
  parent.appendChild(loader);

  // get current tvdbId, and all existing ones in sonarr
  const [tvdbId, existingTvdbIdObj] = await Promise.all([
    getTvdbId(),
    getExistingSonarrTvdbIds(),
  ]);
  const existingTvdbIds = Object.keys(existingTvdbIdObj);

  // remove the loader
  loader.remove();

  // add checkboxes / ribbons
  // couldn't parse tvdb id from somewhere
  if (!tvdbId) {
    const ribbon = document.createElement('div');
    ribbon.className = `btn-sonarr__ribbon ${
      ribbons ? 'btn-sonarr__ribbon--always' : ''
    }`;
    ribbon.innerHTML = `<span>No tvdb</span>`;

    parent.appendChild(ribbon);
    return false;
  }

  // already added to sonarr, ignore!
  if (existingTvdbIds.includes(tvdbId.toString())) {
    const ribbon = document.createElement('div');
    ribbon.className = `btn-sonarr__ribbon btn-sonarr__ribbon--existing ${
      ribbons ? 'btn-sonarr__ribbon--always' : ''
    }`;
    if (sonarrUrl) {
      ribbon.innerHTML = `<a href="${sonarrUrl}/series/${existingTvdbIdObj[tvdbId]}" rel="noreferrer noopener" target="_blank">In Sonarr</a>`;
    }
    else {
      ribbon.innerHTML = `<span>In Sonarr</span>`;
    }

    parent.appendChild(ribbon);
    return false;
  }

  const alwaysShow = await getConfig('alwaysShow', false);

  const checkbox = document.createElement('input');
  checkbox.type = 'checkbox';
  checkbox.name = 'btn-sonarr__checkbox';
  checkbox.className = 'btn-sonarr__checkbox';
  checkbox.value = tvdbId;
  checkbox.checked = alwaysShow;
  checkbox.onchange = () => handleCheckboxChange();

  parent.appendChild(checkbox);

  return true;
}

// ================================= UI Config

/**
 * Close the sonarr settings page
 */
function closeSonarrConfig(overlayElem) {
  if (!overlayElem) {
    return;
  }

  overlayElem.remove();
  document.body.style.overflow = 'inherit';
  window.location.hash = '#close';
}

/**
 * Test a connection to Sonarr
 */
async function testConnection() {
  const textarea = document.getElementById(
    'btn-sonarr__config-testbox-textarea'
  );

  const host = await getConfig('host');
  const key = await getConfig('key');

  textarea.value = '';

  if (!host || !key) {
    textarea.value += 'Error: Please fill in your host and api key';
    return;
  }

  textarea.value += 'Testing connection ...\n\n';

  try {
    const healthCheck = await sonarrGet('/health');
    await setConfig('hasSuccessfulConnection', true);

    if (healthCheck?.length === 0) {
      textarea.value += 'Success! Everything looks good to go\n\n';
    }
    else {
      textarea.value +=
        'Success! Connected to sonarr, but there are health issues!\n\n';

      healthCheck.forEach((h) => {
        textarea.value += ` – ${h.message}\n\n`;
      });
    }

    // TODO: reloads the sonarr bar if needed
    handleCheckboxChange();
    // TODO: reload checkboxes
    addCheckboxesToSeries();
  }
  catch (error) {
    textarea.value += `Error: Couldn't connect to sonarr. Ensure your host and api key are correct\n\n`;
  }
}

/**
 * Create config container to allow user to configure settings
 */
async function createSonarrConfig() {
  const host = await getConfig('host');
  const key = await getConfig('key');
  const ribbons = await getConfig('ribbons', false);
  const alwaysShow = await getConfig('alwaysShow', false);

  const {
    body
  } = document;

  const container = document.createElement('div');
  container.className = 'btn-sonarr__config-container';
  container.innerHTML = `<div class="btn-sonarr__config-header">
  <h3>BTN Sonarr Configuration</h3>
</div>
<div class="btn-sonarr__config-container-content">
  <div class="btn-sonarr__config-desc">
    The Sonarr API Key is attached to the request as a <strong>query string param</strong>. Ensure your Sonarr 
    instance is accessible through any firewalls / auth processes you have in place for this.<br /><br />
    Once you click "Test Connection", a new tab should open asking for permission to connect to your Sonarr domain.
    Click <strong>"Always Allow Domain"</strong>.
  </div>
  <div class="btn-sonarr__config-testbox">
    <label for="btn-sonarr__config-testbox-textarea" class="btn-sonarr__config-label">
      Test Output
      <textarea id="btn-sonarr__config-testbox-textarea" readonly></textarea>
    </label>
    <input type="button" class="btn-sonarr__config-button--test" value="Test Connection" />
  </div>

  <label for="btn-sonarr__config-host" class="btn-sonarr__config-label">
    Sonarr Host: <span class="btn-sonarr__config-label-aside">(inc. port, if applicable)</span>
    <input type="text" class="btn-sonarr__config-input" id="btn-sonarr__config-host" data-config-key="host" placeholder="e.g. https://my.sonarr.com or 127.0.0.1:8989" value="${host}" />
  </label>

  <label for="btn-sonarr__config-apikey" class="btn-sonarr__config-label">
    Sonarr API Key: <span class="btn-sonarr__config-label-aside">(Settings -> General -> API Key)</span>
    <span class="btn-sonarr__config-input--with-icon">
      <input type="password" class="btn-sonarr__config-input" id="btn-sonarr__config-apikey" data-config-key="key" placeholder="e.g. 5b1008a9ce6f35b4cfa8d5b1e0062401" value="${key}" />
      <span class="btn-sonarr__config-input-icon">🔒</span>
    </span>
  </label>

  <label for="btn-sonarr__config-ribbons" class="btn-sonarr__config-label">
    Always show "In Sonarr" and "No tvdb" ribbons:
    <input type="checkbox" class="btn-sonarr__config-checkbox" id="btn-sonarr__config-ribbons" data-config-key="ribbons" ${
      ribbons ? 'checked' : ''
    } />
  </label>

  <label for="btn-sonarr__config-always-show" class="btn-sonarr__config-label">
    Always show Sonarr Bar on page load:
    <input type="checkbox" class="btn-sonarr__config-checkbox" id="btn-sonarr__config-always-show" data-config-key="alwaysShow" ${
      alwaysShow ? 'checked' : ''
    } />
  </label>

  <input type="button" class="btn-sonarr__config-button btn-sonarr__config-button--close" value="Close" />
</div>
`;

  const overlay = document.createElement('div');
  overlay.className = 'btn-sonarr__config-overlay';
  overlay.onclick = (e) => {
    if (e.target !== overlay) {
      return;
    }
    closeSonarrConfig(overlay);
  };
  overlay.appendChild(container);

  body.style.overflow = 'hidden';
  body.appendChild(overlay);

  // vars for event listeners
  const textarea = document.getElementById(
    'btn-sonarr__config-testbox-textarea'
  );

  // event listeners

  // host and api key
  Array.from(document.querySelectorAll('.btn-sonarr__config-input')).forEach(
    (inputElem) => {
      const {
        configKey
      } = inputElem.dataset;
      inputElem.addEventListener('change', async (event) => {
        await setConfig(configKey, event.target.value);
        await setConfig('hasSuccessfulConnection', false);
        textarea.value = `Settings changed: Test Connection to continue using script`;
        const sonarrBar = document.querySelector('.btn-sonarr__bar');
        sonarrBar.dataset.isLoaded = '0';
        handleCheckboxChange();
      });
    }
  );

  // checkboxes
  Array.from(document.querySelectorAll('.btn-sonarr__config-checkbox')).forEach(
    (inputElem) => {
      inputElem.addEventListener('change', async (event) => {
        const {
          checked
        } = event.target;
        const {
          configKey
        } = inputElem.dataset;
        await setConfig(configKey, checked);
        addCheckboxesToSeries();
      });
    }
  );

  // close button
  document
    .querySelector('.btn-sonarr__config-button--close')
    ?.addEventListener('click', () => closeSonarrConfig(overlay));

  // test button
  document
    .querySelector('.btn-sonarr__config-button--test')
    ?.addEventListener('click', testConnection);

  // lock / unlock text input
  /* eslint-disable no-param-reassign */
  Array.from(
    document.querySelectorAll('.btn-sonarr__config-input-icon')
  ).forEach((iconElem) => {
    iconElem.addEventListener('click', () => {
      if (iconElem.innerText === '🔒') {
        iconElem.innerText = '🔓';
        iconElem.previousElementSibling.type = 'text';
      }
      else {
        iconElem.innerText = '🔒';
        iconElem.previousElementSibling.type = 'password';
      }
    });
  });
  /* eslint-enable no-param-reassign */
}

/**
 * Open / close settings depending on url hash
 */
function checkOpenSettings() {
  const overlay = document.querySelector('.btn-sonarr__config-overlay');
  if (window.location.hash === '#sonarr') {
    if (!overlay) {
      createSonarrConfig();
    }
  }
  else if (overlay) {
    closeSonarrConfig(overlay);
  }
}

/**
 * Create the sonarr config tab
 */
function addSonarrConfigTab() {
  const openSettings = () => {
    window.location.hash = '#sonarr';
    checkOpenSettings();
  };

  GM.registerMenuCommand('Open Settings', openSettings, 's');
}

// ================================= CSS

/**
 * Create our custom style tag
 */
function createStyleTag(theme) {
  const css = `
.btn-sonarr__config-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.75);
  z-index: 100;
}

.btn-sonarr__config-container {
  width: 700px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -55%);
  border-radius: 10px;
  padding: 0;
  background-color: #262340;
}

.btn-sonarr__config-container-content {
  padding: 15px;
}

.btn-sonarr__config-header {
  padding: 1px;
  text-align: center;
  font-size: 18px;
  background-color: #2f2b4c;
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
}

.btn-sonarr__config-desc {
  margin-bottom: 10px;
  filter: brightness(85%);
  padding-bottom: 15px;
  border-bottom: 1px dashed #999;
}

.btn-sonarr__config-testbox {
  float: right;
  width: 300px;
}

#btn-sonarr__config-testbox-textarea {
  display: block;
  margin-top: 5px;
  width: 100%;
  height: 235px;
  font-family: monospace;
  font-size: 10px;
}

.btn-sonarr__config-label {
  display: block;
  padding: 8px 0;
}

.btn-sonarr__config-label-aside {
  color: #ccc;
  font-size: 10px;
  padding-left: 5px;
}

.btn-sonarr__config-input {
  display: block; 
  margin-top: 5px;
  width: 340px;
}

.btn-sonarr__config-input-icon {
  position: absolute;
  top: 10px;
  right: 6px;
  font-size: 16px;
}

.btn-sonarr__config-input--with-icon {
  position: relative;
  display: inline-block;
}

.btn-sonarr__config-checkbox {
  margin-top: 5px!important;
  display: block!important;
}

.btn-sonarr__config-button {
  margin-top: 10px!important;
}


.multi-select {
  position: relative;
}

.multi-select__options {
  display: none;
  position: absolute;
  bottom: calc(100% + 3px);
  left: 0;
  border: 1px solid #3a4056;
  background: #2f2b4c;
  border-radius: 2px;
  color: #c0bdda;
  padding: 0.3rem;
  max-width: 200px;
  max-height: 150px;
  overflow-y: scroll;
  z-index: 101;
}

.multi-select__options--opened {
  display: block;
}

.multi-select__option {
  display: block;
  padding: 3px;
  white-space: nowrap;
  overflow: hidden;
}

.multi-select__option-checkbox {
  display: inline-block;
  vertical-align: middle;
}

.multi-select__option-text {
  display: inline-block;
  vertical-align: middle;
  margin-left: 4px;
}

.multi-select__button {
  border: 1px solid ${theme === 'default' ? '#666' : '#373257'};
  background: ${theme === 'default' ? '#333' : '#2f2b4c'};
  border-radius: ${theme === 'default' ? '0px' : '5px'};
  color: ${theme === 'default' ? '#fff' : '#c0bdda'};
  padding: 0.42rem;
  box-shadow: none;
  min-width: 120px;
  max-width: 140px;
  line-height: ${theme === 'default' ? '1' : '1.15'};
  font-weight: normal!important;
  text-align: left;
  margin: 0;
  cursor: default;
  ${theme === 'default' ? 'font-size: 11px' : ''}
}

.multi-select__button:hover {
  background: ${theme === 'default' ? '#333' : '#2f2b4c'};
  animation: none;
  cursor: default;
  ${theme === 'default' ? 'border: 1px solid #aaa;' : ''}
}

.multi-select__button:focus {
  background: ${theme === 'default' ? '#333' : '#2f2b4c'};
  border: 1px solid ${theme === 'default' ? '#aaa' : '#3a4056'};
  box-shadow: none;
}

.multi-select__button-icon {
  float: right;
  position: relative;
  top: -1px;
}


.loading-icon__cont {
  width: 100%;
  height: 70px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.loading-icon__cont a {
  padding: 0 5px;
}

.loading-icon {
  position: relative;
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: #9880ff;
  color: #9880ff;
  animation: dot-flashing 0.75s infinite linear alternate;
  animation-delay: .375s;
}

.loading-icon::before,
.loading-icon::after {
  content: '';
  display: inline-block;
  position: absolute;
  top: 0;
  left: -15px;
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: #9880ff;
  color: #9880ff;
  animation: dot-flashing 0.75s infinite alternate;
  animation-delay: 0s;
}

.loading-icon::after {
  left: 15px;
  animation-delay: 0.75s;
}

@keyframes dot-flashing {
  0% {
    background-color: #9880ff;
  }
  50%,
  100% {
    background-color: #ebe6ff;
  }
}


.btn-sonarr__ribbon {
  width: 60px;
  height: 60px;
  position: absolute;
  left: 10px;
  top: 10px;
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.3s!important;
}

.btn-sonarr__ribbon--always,
.btn-sonarr__ribbon--forced,
*:hover > .btn-sonarr__ribbon {
  opacity: 0.75;
  transition: opacity 0.3s!important;
}

.btn-sonarr__ribbon > a,
.btn-sonarr__ribbon > span {
  position: absolute;
  display: block;
  width: 85px;
  padding: 5px;
  background-color: #3498db;
  box-shadow: 0 5px 10px rgb(0 0 0 / 10%);
  color: #fff;
  text-align: center;
  font-size: 8px;
  right: -1px;
  top: 3px;
  transform: rotate(-45deg);
}

.btn-sonarr__ribbon--existing > a,
.btn-sonarr__ribbon--existing > span {
  background-color: red;
}

.btn-sonarr__checkbox {
  position: absolute!important;
  top: 2px;
  left: 2px;
  opacity: 0;
  transition: opacity 0.3s!important;
}

.btn-sonarr__checkbox--forced,
.btn-sonarr__checkbox:checked,
*:hover > .btn-sonarr__checkbox {
  opacity: 1;
  transition: opacity 0.3s!important;
}


.btn-sonarr__bar {
  display: block;
  position: fixed;
  bottom: 0;
  left: 50%;
  max-width: 1170px;
  width: 100%;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  font-size: 12px;
  text-align: left;
  padding: 0;
  transform: translate(-50%, 100%);
  transition: transform 0.5s;
  background-color: #1e1c33;
  border: 2px solid #443f73;
  border-bottom: transparent;
  z-index: 10;
}

.btn-sonarr__bar--showing {
  transform: translate(-50%, 0%);
  transition: transform 0.5s;
}

.btn-sonarr__bar-label {
  display: inline-block;
  vertical-align: top;
  padding: 10px 9px;
}

.btn-sonarr__bar-label-text {
  display: block;
  margin-bottom: 5px;
}

.btn-sonarr__bar-multi-label {
  display: flex;
  flex-direction: column;
}

.btn-sonarr__bar-multi-label label {
  display: flex;
  gap: 5px;
}

.btn-sonarr__bar-button {
  padding: 0.3rem;
}

.btn-sonarr__bar-select {
  min-width: 120px;
  max-width: 140px;
  padding: 0.3rem;
}

#btn-sonarr__bar-submit {
  min-width: 120px;
}
  `;

  const style = document.createElement('style');
  style.type = 'text/css';
  style.appendChild(document.createTextNode(css));

  document.head.appendChild(style);
}

// ================================= Main Runner

(async function run() {
  createStyleTag(getTheme());
  addSonarrConfigTab();

  // make sure we can add a checkbox to at least one item on the page, before rendering the other things
  const addedCheckboxes = await addCheckboxesToSeries();
  if (addedCheckboxes) {
    addSonarrBar();

    window.addEventListener('hashchange', checkOpenSettings);
    checkOpenSettings();
  }
})();