Mofo

Add links (in torrent description) to external sources for more information.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name           Mofo
// @description    Add links (in torrent description) to external sources for more information.
// @author         _
// @version        0.40
// @grant          GM.xmlHttpRequest
// @connect        www.junodownload.com
// @connect        www.allmusic.com
// @connect        listen.tidal.com
// @connect        api.deezer.com
// @connect        api.discogs.com
// @connect        itunes.apple.com
// @connect        bandcamp.com
// @connect        beatport.com
// @connect        bleep.com
// @connect        qobuz.com
// @connect        bing.com
// @connect        www.bing.com
// @connect        musicbrainz.org
// @match          https://redacted.ch/torrents.php?action=editgroup&groupid=*
// @match          https://redacted.ch/torrents.php?id=*
// @match          https://redacted.ch/user.php?action=edit&*
// @match          https://orpheus.network/torrents.php?action=editgroup&groupid=*
// @match          https://orpheus.network/torrents.php?id=*
// @match          https://orpheus.network/user.php?action=edit&*
// @run-at         document-end
// @namespace      _
// ==/UserScript==

(() => {
  "use strict";

  const myUID = document.querySelector("#nav_userinfo > a.username").href.split("=")[1];
  const MofoConfig = JSON.parse(localStorage.getItem("mofo_config") || '{"mofo_summary": "Mofo", "discogs_token": "", "allmusic_review": false, "before_links": false}');

  if (location.href.includes(`action=edit&userid=${myUID}`)) {
    const updateMofoConfig = (e) => {
      const config = {};
      config.before_links = document.getElementById("before_links").checked;
      config.allmusic_review = document.getElementById("allmusic_review").checked;
      config.discogs_token = document.getElementById("discogs_token").value;
      config.mofo_summary = document.getElementById("mofo_summary").value;
      localStorage.setItem("mofo_config", JSON.stringify(config));
    };
    // ref: pootie's External Stylesheet Switcher
    const torGrouping = $("#tor_group_tr");
    const MofoSummaryRow = $(`
      <tr>
        <td class="label tooltip">
          <strong>Mofo Summary Text:</strong>
        </td>
      </tr>
    `);
    const ReviewPosition = $(`
      <li>
        <input type="checkbox" name="before_links" id="before_links">
        <label for="showtfilter">Append AllMusic Review Before Mofo Links</label>
      </li>
    `);
    const AllMusicRow = $(`
      <tr>
        <td class="label tooltip">
          <strong>Mofo AllMusic:</strong>
        </td>
      </tr>
    `);
    const AllMusicCol = $(`<td></td>`);
    const AllMusicUl = $(`<ul class="options_list nobullet"></ul>`);
    const AllMusicReviews = $(`
      <li>
        <input type="checkbox" name="allmusic_review" id="allmusic_review">
        <label for="showtfilter">Include review in torrent description</label>
      </li>
    `);
    AllMusicUl.append([AllMusicReviews,ReviewPosition]);
    AllMusicCol.append(AllMusicUl);
    AllMusicRow.append(AllMusicCol);
    const MofoSummaryCol = $(`<td></td>`);
    const MofoSummaryTxt = $(`<input type="text" size="40" name="mofo_summary" id="mofo_summary" value="${MofoConfig.mofo_summary}">`)
    const DiscogsTokenRow= $(`
      <tr>
        <td class="label tooltip">
          <strong>Mofo Discogs Token:</strong>
        </td>
      </tr>
    `);
    const DiscogsTokenCol = $(`<td></td>`);
    const DiscogsTokenTxt = $(`<input type="text" size="40" name="discogs_token" id="discogs_token" value="${MofoConfig.discogs_token}">`)
    MofoSummaryCol.append(MofoSummaryTxt);
    MofoSummaryRow.append(MofoSummaryCol);
    torGrouping.after(MofoSummaryRow);
    DiscogsTokenCol.append(DiscogsTokenTxt);
    DiscogsTokenRow.append(DiscogsTokenCol);
    torGrouping.after(DiscogsTokenRow);
    torGrouping.after(AllMusicRow);
    document.getElementById("before_links").checked = MofoConfig.before_links;
    document.getElementById("allmusic_review").checked = MofoConfig.allmusic_review;
    document.getElementById("userform").addEventListener("submit", updateMofoConfig);
    return;
  }

  const groupid = new URL(window.location).searchParams.get("groupid");
  if (!groupid) {
    // we are on a torrent page
    const id = new URL(window.location).searchParams.get("id");
    const editDescr = document.querySelector("div.header > .linkbox").firstElementChild;
    editDescr.insertAdjacentHTML("afterend", ` <a href=torrents.php?action=editgroup&groupid=${id}&mofo=true class="brackets">Mofo</a>`);
    return;
  }

  const promises = [];
  const previewBtn = document.querySelector('.button_preview_0');

  $('head').append('<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">');
  previewBtn.insertAdjacentHTML(
    "afterend",
    ` <i style="display: none; padding: 10px;" id="spinner" class="fa fa-spinner fa-spin"></i><input type="button" value="Mofo" id="button_mofo" title="More Info">`
  );

  const decodeHTML = orig => {
    const txt = document.createElement("textarea");
    txt.innerHTML = orig;
    return txt.value;
  };

  const sanitize = text => {
    return text
      .replace(/\s+\(?EP\)?$/i, "")
      .replace(/\s+\(cover\)$/i, "")
      .toLowerCase()
      .trim();
  };

  const gazelleAPI = async groupid => {
    try {
      const res = await fetch(`/ajax.php?action=torrentgroup&id=${groupid}`);
      return res.json();
    } catch (error) {
      console.log(error);
    }
  };

  const corsFetch = url => {
    const headers = {};
    if (url.includes("tidal")) {
      headers['x-tidal-token'] = "CzET4vdadNUFQ5JU";
    }
    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        method: "GET",
        url: encodeURI(url),
        headers: headers,
        onload: res => resolve(res.responseText),
        onerror: res => reject(res)
      });
    });
  };

  const fetchJSON = async url => {
    const responseText = await corsFetch(url);
    return JSON.parse(responseText);
  };

  const fetchDOM = async url => {
    const responseText = await corsFetch(url);
    const parser = new DOMParser();
    return parser.parseFromString(responseText, "text/html");
  };

  const appleSearch = async (artist, album, upc) => {
    try {
      let response = await fetchJSON(`https://itunes.apple.com/lookup?upc=${upc}&entity=album`);
      let itunes_id;
      if (response.results.length > 0) {
        itunes_id = response.results[0].collectionId;
      }
      if (itunes_id) {
        return {source: "Apple", url: `https://music.apple.com/album/${itunes_id}`}
      }
      console.log("Apple: UPC search failed; trying direct artist - title query.")
      response = await fetchJSON(`https://itunes.apple.com/search?term=${artist} - ${album}&entity=album`);
      if (response.results.length > 0) {
        const collection = response.results.find(release => sanitize(artist).includes(sanitize(release.artistName)) && sanitize(album).includes(sanitize(release.collectionName)));
        itunes_id = collection.collectionId;
      }
      if (itunes_id) {
        return {source: "Apple", url: `https://music.apple.com/album/${itunes_id}`}
      }
    } catch (e) {
      console.error(
        `Oops! Apple search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const searchDiscogs = async (artist, album) => {
    try {
      const response = await fetchJSON(`https://api.discogs.com/database/search?release_title=${album}&artist=${artist}&token=${MofoConfig.discogs_token}`);
      if (response.results.length == 0) console.log('Discogs: no match.')
      const master = response.results.find(release => sanitize(release.title).includes(sanitize(artist)) && sanitize(release.title).includes(sanitize(album)));
      if (master) {
        if (master.master_id) return {source: "Discogs", url: `https://www.discogs.com/master/${master.master_id}`};
        return {source: "Discogs", url: `https://www.discogs.com${master.uri}`};
      }
    } catch (e) {
      console.error(
        `Oops! Discogs search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const deezSearch = async (artist, album, upc) => {
    try {
      let response = await fetchJSON(`https://api.deezer.com/album/upc:${upc}`);
      let deezer_id = response.hasOwnProperty("error") ? false : response.id;
      if (deezer_id) {
        return {source: "Deezer", url: `https://www.deezer.com/en/album/${deezer_id}`}
      }
      console.log("Deezer: UPC search failed; querying API directly for artist - title.")
      response = await fetchJSON(
        `https://api.deezer.com/search?q=artist:"${sanitize(artist)}" album:"${sanitize(album)}"`
      );
      const found_album = response.data.find(release => sanitize(artist).includes(sanitize(release.artist.name)) && sanitize(album).includes(sanitize(release.album.title)));
      if (found_album) {
        return {source: "Deezer", url: `https://www.deezer.com/en/album/${found_album.album.id}`}
      }
    } catch (e) {
      console.error(
        `Oops! Deezer search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const qobuzSearch = async (artist, album) => {
    try {
      const query = `${artist} - ${album}`;
      const doc = await fetchDOM(
        `https://qobuz.com/nz-en/search?q=${query}`
      );
      const releases = [...doc.querySelectorAll('#main-column > div.search-results > div > div.detail')];
      const found_album = releases.find(release => {
        if (sanitize(release.querySelector('.artist-name').innerText).includes(sanitize(artist))) {
          return sanitize(release.querySelector('.album-title').innerText).includes(sanitize(album));
        }
      });
      const purchaseLink = found_album.querySelector('div.price-box > div.action > ul > li:nth-child(1) > a').href;
      if (purchaseLink) {
        return {source: "Qobuz", url: purchaseLink.replace(location.origin, "https://www.qobuz.com")};
      }
    } catch (e) {
      console.error(`Oops! Qobuz search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  // bleep's internal search is cumbersome; better luck using bing
  const bleepSearch = async (artist, album) => {
    try {
      const doc = await fetchDOM(
        `https://bing.com/search?q=${artist} ${album} site:bleep.com`
      );
      const nodes = [...doc.querySelectorAll('li.b_algo h2 > a')];
      const found_album = nodes.find(
        item =>
          sanitize(item.innerText).includes(sanitize(album)) &&
          sanitize(item.innerText).includes(sanitize(artist)));
      const bingObfuscate = await corsFetch(found_album.href);
      const bleepString = bingObfuscate.match(/bleep\.com\/release\/([^";]+)/);
      if (bleepString && bleepString.length == 2) {
        return {source: "Bleep", url: `https://bleep.com/release/${bleepString[1]}`};
      }
    } catch (e) {
      console.error(
        `Oops! Bleep search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  // spotify requires account/api key, so using bing
  const spotifySearch = async (artist, album) => {
    try {
      const doc = await fetchDOM(
        `https://bing.com/search?q=${artist} ${album} site:open.spotify.com/album`
      );
      const nodes = [...doc.querySelectorAll('li.b_algo h2 > a')];
      const found_album = nodes.find(
        item =>
          sanitize(item.innerText).includes(sanitize(album)) &&
          sanitize(item.innerText).includes(sanitize(artist)));
      const bingObfuscate = await corsFetch(found_album.href);
      const spotifyString = bingObfuscate.match(/album\/([a-zA-Z0-9]+)/);
      if (spotifyString && spotifyString.length == 2) {
        return {source: "Spotify", url: `https://open.spotify.com/album/${spotifyString[1]}`};
      }
    } catch (e) {
      console.error(
        `Oops! Spotify search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  // bandcamp's internal search is cumbersome; try using bing first
  const bandSearch = async (artist, album) => {
    try {
      let doc = await fetchDOM(
        `https://bing.com/search?q=site:bandcamp.com/album ${artist} ${album}`
      );
      const nodes = [...doc.querySelectorAll('li.b_algo h2 > a')];
      const found_album = nodes.find(
        item =>
          sanitize(item.innerText).includes(sanitize(album)) &&
          sanitize(item.innerText).includes(sanitize(artist)));
      const bingObfuscate = await corsFetch(found_album.href);
      const bandcampLink = bingObfuscate.match(/"(https:\/\/.+bandcamp\.com.+)"/);
      if (bandcampLink && bandcampLink.length == 2) {
        return {source: "Bandcamp", url: `${bandcampLink[1]}`};
      } else {
        // fall back to BC search
        console.log("Bandcamp: Bing failed; attempting direct search.")
        doc = await fetchDOM(`https://bandcamp.com/search?q=${artist} - ${album}`);
        for (const result of doc.querySelectorAll('.searchresult.album')) {
          let albumTitle = result.querySelector('div.result-info > .heading').innerText.trim();
          let albumArtist = result.querySelector('div.result-info > .subhead').innerText.trim().replace("by ", "");
          if (sanitize(albumTitle).includes(sanitize(album)) && sanitize(albumArtist).includes(sanitize(artist))) {
            let directLink = result.querySelector('div.result-info > .heading > a').href.split('?')[0]
            return {source: "Bandcamp", url: directLink};
          }
        }
      }
      const bclink = found_album.href;
      if (bclink && bclink.includes("bandcamp")) {
        return {source: "Bandcamp", url: bclink.split('?')[0]};
      }
    } catch (e) {
      console.error(
        `Oops! Bandcamp search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const beatSearch = async (artist, album) => {
    try {
      const query = `${artist} - ${album}`;
      const doc = await fetchDOM(
        `https://www.beatport.com/search?q=${query}`
      );
      const releases = [...doc.querySelectorAll('ul.bucket-items > li.bucket-item.ec-item.release')];
      const found_album = releases.find(release => {
        if (sanitize(release.querySelector('p.release-artists').innerText).includes(sanitize(artist))) {
          return sanitize(release.querySelector('p.release-title').innerText).includes(sanitize(album));
        }
      });
      const purchaseLink = found_album.querySelector('p.release-title > a').href;
      if (purchaseLink) {
        return {source: "Beatport", url: purchaseLink.replace(location.origin, "https://beatport.com")};
      }
    } catch (e) {
      console.error(`Oops! Beatport search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const junoSearch = async (artist, album) => {
    try {
      const query = `${artist} - ${album}`;
      const doc = await fetchDOM(
        `https://www.junodownload.com/search/?solrorder=relevancy&q[all][]=${query}&track_sale_format=flac`
      );
      const artists = [...doc.querySelectorAll(".juno-artist")];
      const found_artists = artists.filter(node =>
        sanitize(node.innerText).includes(sanitize(artist))
      );
      const found_album = found_artists.find(
        artist =>
          sanitize(artist.parentNode.nextElementSibling.querySelector(".juno-title").innerText) === sanitize(album)
      );
      const parentNode = found_album.parentNode.parentNode;
      const purchaseLink = "https://" + parentNode
        .querySelector(".juno-title")
        .href.replace(`${location.origin}`, "www.junodownload.com");
      if (purchaseLink) {
        return {source: "Juno", url: purchaseLink};
      }
    } catch (e) {
      console.error(`Oops! Juno search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const allMusicSearch = async (artist, album) => {
    try {
      let review = false;
      const query = `${artist} ${album}`;
      let doc = await fetchDOM(`https://www.allmusic.com/search/all/${query}`);
      const albums = [...doc.querySelectorAll("div.results li.album")];
      const found_album = albums.find(
        captured => sanitize(captured.querySelector('.title').innerText).includes(sanitize(album)) && sanitize(captured.querySelector('.artist').innerText).includes(sanitize(artist))
      );
      const allMusicLink = found_album.querySelector('div.title > a')
        .href.replace(`${location.origin}`, "www.allmusic.com");
      if (MofoConfig.allmusic_review) {
        doc = await fetchDOM(allMusicLink);
        const authorElem = doc.querySelector('div.review-headline-container > p > span');
        const reviewElem = [...doc.querySelectorAll('section.read-more > div.text > p')];
        if (authorElem && reviewElem) {
          review = {author: authorElem.innerText.trim(), text: reviewElem.map((e) => e.innerText.trim()).join("\n\n")}
        }
      }
      return {source: "AllMusic", url: allMusicLink, review: review};
    } catch (e) {
      console.error(`Oops! AllMusic search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const tidalSearch = async (artist, album, upc) => {
    try {
      const query = `${artist} - ${album}`;
      const response = await fetchJSON(`https://listen.tidal.com/v1/search?query=${query}&limit=10&offset=0&types=ALBUMS&includeContributors=true&countryCode=US`);
      for (const release of response.albums.items) {
        if (upc && release['upc'].includes(upc)) {
          return {source: "Tidal", url: `https://tidal.com/browse/album/${release['url'].split("/").pop()}`};
        } else if (sanitize(release['artists'][0]['name']) == sanitize(artist) && sanitize(release['title']) == sanitize(album)) {
          return {source: "Tidal", url: `https://tidal.com/browse/album/${release['url'].split("/").pop()}`};
        }
      }
    } catch (e) {
      console.error(`Oops! Tidal search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const musicBrainzSearch = async (artist, album) => {
    try {
      const response = await fetchJSON(`https://musicbrainz.org/ws/2/release-group?query="${album}" AND artist:${artist}&fmt=json`);
      if (response['release-groups'].length == 0) console.log("MusicBrainz: no match.");
      for (const release of response['release-groups']) {
        if (!sanitize(release.title).includes(sanitize(album))) continue;
        for (const mbartist of release['artist-credit']) {
          if (sanitize(mbartist.name).includes(sanitize(artist))) {
            return {source: "MusicBrainz", url: `https://musicbrainz.org/release-group/${release.id}`};
          }
        }
      }
    } catch (e) {
      console.error(`Oops! MusicBrainz search failed (probably, release not found)...${e && e.message && `\n(${e.message})`}`
      );
    }
  };

  const searchForLinks = async (event) => {
    event.target.removeEventListener("click", searchForLinks);
    document.querySelector('#button_mofo').style.display = "none";
    document.querySelector('#spinner').style.display = "";
    gazelleAPI(groupid).then(({ response, status }) => {
      if (status !== "success") {
        console.log("API request failed; aborting.");
        return;
      } 
      const artist = decodeHTML(response.group.musicInfo.artists[0].name);
      const album = decodeHTML(response.group.name);
      const torrents = response.torrents.filter(torrent => /\b(\d{10,})\b/.test(torrent.remasterCatalogueNumber));
      let upc;
      if (torrents.length != 0) {
        upc = torrents[0].remasterCatalogueNumber.match(/\b(\d{10,})\b/);
      }
      if (upc && upc.length == 2) {
        upc = upc[1];
      }
      promises.push(deezSearch(artist, album, upc));
      promises.push(allMusicSearch(artist, album));
      promises.push(appleSearch(artist, album, upc));
      promises.push(bandSearch(artist, album));
      promises.push(bleepSearch(artist, album));
      promises.push(junoSearch(artist, album));
      promises.push(beatSearch(artist, album));
      promises.push(qobuzSearch(artist,album));
      promises.push(searchDiscogs(artist,album));
      promises.push(musicBrainzSearch(artist,album));
      promises.push(spotifySearch(artist,album));
      promises.push(tidalSearch(artist, album, upc));
      Promise.all(promises).then((results) => {
        const moreInfo = [];
        results.sort((a, b) => {
          var sourceA = a.source.toUpperCase();
          var sourceB = b.source.toUpperCase();
          if (sourceA < sourceB) {
            return -1;
          }
          if (sourceA > sourceB) {
            return 1;
          }
          return 0;
        });
        results.forEach(result => {
          if (!result) return;
            moreInfo.push(`[url=${result.url}]${result.source}[/url]`)
        });
        if (moreInfo.length > 0) {
          const allMusicReview = results.find(result => result && result.source == "AllMusic" && result.review);
          if (allMusicReview && MofoConfig.before_links) {
            groupDesc.value += `\n\n[quote=AllMusic]${allMusicReview.review.text}\n[align=right]- ${allMusicReview.review.author}[/align][/quote]`
          } else {
            groupDesc.value += "\n\n";
          }
          groupDesc.value += "[b]More info:[/b] " + moreInfo.join(" | ");
          if (allMusicReview && !MofoConfig.before_links) {
            groupDesc.value += `\n\n[quote=AllMusic]${allMusicReview.review.text}\n[align=right]- ${allMusicReview.review.author}[/align][/quote]`
          }
          document.getElementsByName('summary')[0].value = MofoConfig.mofo_summary;
        }
        groupDesc.style.height = groupDesc.scrollHeight + "px";
        if ([...document.getElementById('preview_wrap_0').classList].includes("hidden")) {
          previewBtn.click();
        } else {
          previewBtn.click();
          groupDesc.style.height = groupDesc.scrollHeight + "px";
          previewBtn.click();
        }
        document.querySelector('#spinner').style.display = "none";
      });
    });
  }
  const groupDesc = document.querySelector('#textarea_wrap_0 > textarea');
  groupDesc.style.height = groupDesc.scrollHeight + "px";
  document.getElementById("button_mofo").addEventListener("click", searchForLinks);
  const autoMofo = new URL(window.location).searchParams.get("mofo");
  if (autoMofo) {
    document.getElementById("button_mofo").click();
  }
})();