<h1>Analyseur Top iTunes et Podcastéo</h1>
<div id="container">
  <div id="selector">
  <select id="pays_selector">
  <option value="podcasteo">Annuaire Podcastéo</option>
   <option value="podcasteo200">Top 200 Podcastéo</option>
  <option value="fr">Top 200 iTunes France</option>
  <option value="ue_uk">Top 200 iTunes UE + UK</option>
  <option value="franco">Top 200 iTunes Francophonie</option>
  <option value="us">Top 200 iTunes US</option>
  <option value="all"> Top 200 iTunes US + UE + UK + Francophonie</option>
   <option value="podcloud">Catalogue podCloud (export du 18 avril 2019) (lent)</option>
</select>
  <button id="fetch_data">Analyser</button>
  <div id="top_itunes">
  <label for="pays">Top iTunes à analyser&nbsp;:</label>
  <textarea id="pays" rows=2 cols=40></textarea>
  <div><small> (séparés par des virgules)</small></div>
  </div>
  </div>
  <div id="results">
    <div id="progress">
    </div>
    <label>Résultat de l'analyse :</label>
    <div id="resultats">
    </div>
  </div>
</div>
html {
  font-family: Lato, Helvetica, "sans-serif", sans;
  
}

label {
  display: block;
  margin: 10px 0;
  font-weight: bold;
}

textarea {
  margin: 5px;
}

#fetch_data {
  width: 150px;
  margin: 0 10px
  padding: 5px;
}

#container {
  padding: 0 20px;
}

li {
  list-style-type: none;
  padding-left: 20px;
}

li span {
  display: inline-block;
  cursor: pointer;
  line-height: 18px;
  font-size: 15px;
  height: 20px;
}

li span em {
  display: inline-block;
  margin: 0 5px;
}

li span::before {
  content: "▸";
}

li.open span::before {
  content: "▾";
}

li span:hover, li span:focus {
  opacity: 0.8;
}

li ul {
  margin: 2px;
  padding: 0;
  display: none;
}

li.open ul {
  display: block;
}

li ul li {
  padding-left: 10px;
}

#fetch_data {
  display: inline-block;
  margin: 15px;
}

#selector,
#results {
  padding: 15px;
}
Queue.configure(window.Promise);

/**
 * Retourne une fonction qui, tant qu'elle est appelée,
 * n'est exécutée au plus qu'une fois toutes les N millisecondes.
 * Paramètres :
 *  - func : la fonction à contrôler
 *  - wait : le nombre de millisecondes (période N) à attendre avant
 *           de pouvoir exécuter à nouveau la function func()
 *  - leading (optionnel) : Appeler également func() à la première
 *                          invocation (Faux par défaut)
 *  - trailing (optionnel) : Appeler également func() à la dernière
 *                           invocation (Faux par défaut)
 *  - context (optionnel) : le contexte dans lequel appeler func()
 *                          (this par défaut)
 */
function throttle(func, wait, leading, trailing, context) {
  var ctx, args, result;
  var timeout = null;
  var previous = 0;
  var later = function() {
    previous = new Date();
    timeout = null;
    result = func.apply(ctx, args);
  };
  return function() {
    var now = new Date();
    if (!previous && !leading) previous = now;
    var remaining = wait - (now - previous);
    ctx = context || this;
    args = arguments;
    // Si la période d'attente est écoulée
    if (remaining <= 0) {
      // Réinitialiser les compteurs
      clearTimeout(timeout);
      timeout = null;
      // Enregistrer le moment du dernier appel
      previous = now;
      // Appeler la fonction
      result = func.apply(ctx, args);
    } else if (!timeout && trailing) {
      // Sinon on s’endort pendant le temps restant
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
}

const uniqueOnly = (value, index, self) => self.indexOf(value) === index;

const groupBy = (xs, key) =>
  xs.reduce((rv, x) => {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});

const opened = {};

const pays_ue_uk = [
  "at",
  "be",
  "bg",
  "cy",
  "cz",
  "de",
  "dk",
  "ee",
  "es",
  "fi",
  "fr",
  "gb",
  "hr",
  "hu",
  "ie",
  "it",
  "lt",
  "lu",
  "mt",
  "nl",
  "pl"
];

const pays_franco = [
  "fr",
  "be",
  "ch",
  "lu",
  "ca",
  "tn",
  "dz",
  "lb",
  "mg",
  "sn"
];

const pays_full = pays_ue_uk
  .concat(pays_franco)
  .concat(["us"])
  .filter(uniqueOnly);

const select_list = e => {
  const list_code = e.target.options[e.target.selectedIndex].value;

  list_text.value = (list_code === "podcloud"
    ? ["podcloud"]
    : list_code === "podcasteo"
      ? ["podcasteo"]
      : list_code === "podcasteo200"
        ? ["podcasteo200"]
        : list_code == "fr"
          ? ["fr"]
          : list_code == "us"
            ? ["us"]
            : list_code == "franco"
              ? pays_franco
              : list_code == "ue_uk" ? pays_ue_uk : pays_full
  ).join(", ");

  document.getElementById(
    "top_itunes"
  ).style.display = /podcasteo|podcloud/.test(list_code) ? "none" : "block";
};

const list_text = document.getElementById("pays");
const list_selector = document.getElementById("pays_selector");

list_selector.addEventListener("change", select_list);
select_list({ target: list_selector });

// Utilise un proxy pour permettre les requêtes depuis le navigateur :
// Contenu du fichier :
// <?php header('Access-Control-Allow-Origin: *'); echo file_get_contents($_REQUEST['url']);
const cors_proxy = url =>
  `https://podshows.fr/45df412r.php?url=${encodeURIComponent(url)}`;

const getHosting = url => ({
  url,
  hoster: [
    { name: "podCloud", regex: /(podcloud|lepodcast)\.fr/ },
    { name: "Ausha", regex: /ausha\.co/ },
    { name: "Pippa", regex: /pippa\.io/ },
    { name: "SoundCloud", regex: /feeds\.soundcloud\.com/ },
    { name: "Art19", regex: /rss\.art19\.com/ },
    { name: "Libsyn", regex: /libsyn(pro)?\.com/ },
    { name: "Acast", regex: /rss\.acast\.com/ },
    { name: "Megaphone", regex: /megaphone\.fm/ },
    { name: "Anchor", regex: /anchor\.fm/ },
    { name: "Spreaker", regex: /spreaker\.com/ },
    { name: "FeedBurner", regex: /feedburner\.com/ },
    { name: "Buzzsprout", regex: /buzzsprout\.com/ },
    { name: "Omnycontent", regex: /(omnycontent\.com|omni\.fm)/ },
    { name: "Whooshkaa", regex: /whooshkaa\.com/ },
    { name: "Castfire", regex: /castfire\.com/ },
    { name: "Transistor", regex: /transistor\.fm/ },
    { name: "Castfire", regex: /castfire\.com/ },
    { name: "Audioboom", regex: /audioboom\.com/ },
    { name: "Podbean", regex: /podbean\.com/ },
    { name: "Squarespace", regex: /squarespace\.com/ },
    { name: "podomatic", regex: /pod(o|0)matic\.com/ },
    { name: "djpod", regex: /feeds\.djpod\.com/ },
    { name: "rss.com", regex: /rss\.com/ },
    { name: "JellyCast", regex: /jellycast\.com/ },
    { name: "PodcastMirror", regex: /podcastmirror\.com/ },
    { name: "BackdoorPodcasts", regex: /backdoorpodcasts\.com/ },
    { name: "Fireside", regex: /fireside\.fm/ },
    { name: "Podiant", regex: /podiant\.co/ },
    { name: "blubrry", regex: /blubrry\.com/ },
    { name: "rapidfeeds.com", regex: /rapidfeeds\.com/ },
    { name: "podcaster.de", regex: /(podcaster|podspot)\.de/ },
    { name: "hipcast.com", regex: /hipcast\.com/ },
    { name: "simplecast.com", regex: /(rss|feeds)\.simplecast\.com/ },
    { name: "podigee", regex: /podigee\.io/ },
    { name: "Podfm.ru", regex: /podfm\.ru/ },
    {
      name: "Apple",
      regex: /(applehosted\.podcasts|itunesu\.itunes)\.apple\.com/
    },
    { name: "podcasts.com", regex: /podcasts\.com\/rss_feed/ },
    { name: "podcast.co", regex: /(feed\.pod|podcast).co\// },
    { name: "ivoox.com", regex: /ivoox\.com/ },
    { name: "audioblog.arteradio.com", regex: /audioblog\.arteradio\.com/ },
    { name: "hearthis.at", regex: /hearthis\.at/ }
  ].reduce(
    (found_hoster, hoster) =>
      hoster.regex.test(url) ? hoster.name : found_hoster,
    "Autre/Interne"
  )
});

const progress = document.getElementById("progress");
const resultats = document.getElementById("resultats");

let running = false;
let result_urls = {};
let result_urls_by_hoster = {};
let errors = 0;

const queue = new Queue(15);

const immediateStatus = () =>
  window.setTimeout(() => {
    if (queue.pendingPromises > 0 || queue.queue.length > 0) {
      progress.innerHTML = "Opérations en cours : " + queue.pendingPromises;
      progress.innerHTML += "<br>Opérations en attente : " + queue.queue.length;
    } else {
      progress.innerHTML = "Terminé.";
      progress.innerHTML +=
        "<br>" + Object.keys(result_urls).length + " flux trouvés. ";
      if (errors) {
        progress.innerHTML +=
          "<br>" +
          errors +
          " erreur(s). (l'API iTunes ne renvoit pas l'URL du flux) ";
      }

      running = false;
    }

    const count_by_host = Object.keys(result_urls_by_hoster)
      .map(k => ({
        hoster: k,
        size: result_urls_by_hoster[k].length,
        percent: (
          result_urls_by_hoster[k].length *
          100 /
          Object.keys(result_urls).length
        )
          .toFixed(3)
          .replace(/0+$/, "")
          .replace(/\.$/, "")
      }))
      .sort((a, b) => b.size - a.size);

    resultats.innerHTML = `
      <ul id='hosters'>
        ${count_by_host
          .map(hoster => {
            const liid = "hoster-" + hoster.hoster.replace(/[^A-z]/, "-");
            return `
            <li id="${liid}" class="${opened[liid] ? "open" : ""}">
              <span onclick="toggleVisibility('${liid}');">
                ${hoster.hoster}<em>;</em>${hoster.size}<em>;</em>${
              hoster.percent
            }%
              </span>
              ${
                opened[liid]
                  ? `
              <ul>
                ${result_urls_by_hoster[hoster.hoster]
                  .map(
                    feed => `
                  <li>
                    <a href="${feed.url}" target="_blank">
                      <pre>${feed.url}</pre>
                    </a>
                  </li>
                `
                  )
                  .join("")}
              </ul>
              `
                  : ""
              }
            </li>
          `;
          })
          .join("")}
    </ul>
    ${
      !running
        ? `
      <label>Tous les flux:</label>
      <pre style="width: 50%; height: 150px; overflow: scroll;">${Object.keys(
        result_urls
      ).join("\n")}</pre>
    `
        : ""
    }
  `;
  }, 0);

const status = throttle(immediateStatus, 1000, true, true);

const analyze_data = () => {
  if (running) return alert("Déjà en cours");
  running = true;

  progress.innerHTML = "";
  result_urls = {};
  result_urls_by_hoster = {};
  errors = 0;
  resultats.innerHTML = "";

  const countries = list_text.value.split(",").map(c => c.trim());

  const fetch_itunes_top = country =>
    fetch(
      cors_proxy(
        `https://rss.itunes.apple.com/api/v1/${country}/podcasts/top-podcasts/all/200/explicit.json`
      )
    )
      .then(res => res.json())
      .then(res => {
        return res.feed.results.map(p => p.url);
      });

  const fetch_podcasteo = () =>
    fetch(cors_proxy(`http://www.podcasteo.fr/annuaire.php`))
      .then(body => body.text())
      .then(body =>
        body
          .match(/<span class="username">\s+<a href=".*" target="_blank">/g)
          .map(m => {
            const match = m.match(/\"(https?:\/\/[^"]+)\"/);
            return match && match[1];
          })
          .filter(Boolean)
      );

  const fetch_top_podcasteo = () =>
    fetch(cors_proxy(`http://www.podcasteo.fr/classement.php`))
      .then(body => body.text())
      .then(body =>
        body
          .match(
            /<div class="box box-widget widget-user">\s+<a href=".*" target="_blank" data-toggle="modal">/g
          )
          .map(m => {
            const match = m.match(/\"(https?:\/\/[^"]+)\"/);
            return match && match[1];
          })
          .filter(Boolean)
          .slice(0, 200)
      );

  const fetch_podcloud = () =>
    fetch(
      cors_proxy(
        `http://assets.podcloud.fr/catalogue/catalogue_podCloud_2019_04_18_6989_podcasts.json`
      )
    )
      .then(body => body.json())
      .then(list => list.map(p => p.RSS).filter(Boolean));

  const add_feed_to_result = feedUrl => {
    if (!result_urls[feedUrl]) {
      const feed = getHosting(feedUrl);
      result_urls[feedUrl] = feed;
      (result_urls_by_hoster[feed.hoster] =
        result_urls_by_hoster[feed.hoster] || []).push(feed);
    }

    status();
  };

  const add_itunes_feed_to_result = url => {
    var match = url.match(/id(\d+)/);
    if (!match) match = url.match(/\d+/); // 123456

    if (!match) {
      console.error(
        "Could not find Apple object ID for : " + url,
        `${++errors} error(s)`
      );
      return false;
    }

    var artID = match[1];

    queue.add(() => {
      return fetch(
        cors_proxy(
          `https://itunes.apple.com/lookup?id=${parseInt(artID)}&entity=podcast`
        )
      )
        .then(res => {
          try {
            return res.json();
          } catch (e) {
            console.error(
              "Could not parse iTunes response for feed : " + url,
              res,
              `${++errors} error(s)`,
              e
            );
          }

          return null;
        })
        .then(body => {
          if (!body)
            return console.error(
              "No match for feed : " + url,
              `${++errors} error(s)`
            );

          if (!body.results || !body.results[0])
            return console.error(
              "No result in body for feed : " + url,
              artID,
              body,
              `${++errors} error(s)`
            );

          const feedUrl = body.results[0].feedUrl;

          if (!(typeof feedUrl === "string"))
            return console.error(
              "Feed URL is not a string in result for feed  : " + url,
              body,
              `${++errors} error(s)`
            );

          add_feed_to_result(feedUrl);
        });
    });
  };

  countries.forEach(c =>
    queue.add(() => {
      status();

      const fetch_list =
        c === "podcloud"
          ? fetch_podcloud()
          : c === "podcasteo"
            ? fetch_podcasteo()
            : c === "podcasteo200"
              ? fetch_top_podcasteo()
              : fetch_itunes_top(c);

      return fetch_list.then(list => {
        list.forEach(url =>
          queue.add(() => {
            if (typeof url === "string") {
              if (/^https?:\/\/(podcasts|itunes)\.apple\.com/.test(url)) {
                add_itunes_feed_to_result(url);
              } else {
                add_feed_to_result(url);
              }
            } else {
              console.error("URL was not a string : ", url);
            }
            status();
          })
        );
      });
    })
  );

  status();
};

document.getElementById("fetch_data").addEventListener("click", analyze_data);

window.toggleVisibility = id => {
  console.log(id);
  const el = document.getElementById(id);
  console.log(el);
  el.classList.toggle("open");
  opened[id] = el.classList.contains("open");
  immediateStatus();
};
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://rawgit.com/azproduction/promise-queue/master/lib/index.js