aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorCiaran Gallagher <[email protected]>2019-11-28 18:06:15 +0000
committerCiaran Gallagher <[email protected]>2019-11-28 18:06:15 +0000
commite118c68d12c82e4e7fda20244dda05c7f8f4bb79 (patch)
tree48b10d85821539cc9d356872cf06d1a98124b565
parentadd7b7e8ebc881e3e92e03af46ff08059a7687da (diff)
downloadpasta-e118c68d12c82e4e7fda20244dda05c7f8f4bb79.tar.gz
pasta-e118c68d12c82e4e7fda20244dda05c7f8f4bb79.zip
Initial Commit
-rw-r--r--css/main.css51
-rw-r--r--index.html260
-rw-r--r--js/main.js593
3 files changed, 904 insertions, 0 deletions
diff --git a/css/main.css b/css/main.css
new file mode 100644
index 0000000..016b298
--- /dev/null
+++ b/css/main.css
@@ -0,0 +1,51 @@
+#libraryTable tbody tr {
+ cursor: pointer;
+}
+
+#tvShowsTable tbody tr {
+ cursor: pointer;
+}
+
+#seasonsTable tbody tr {
+ cursor: pointer;
+}
+
+#episodesTable tbody tr {
+ cursor: pointer;
+}
+
+#audioTable tbody tr {
+ cursor: pointer;
+}
+
+#audioTable tbody tr.table-active:not(.success-transition) {
+ transition: background-color 2s;
+}
+
+#audioTable tbody tr.success-transition {
+ background-color: #c3e6cb;
+}
+
+#subtitleTable tbody tr {
+ cursor: pointer;
+}
+
+#subtitleTable tbody tr.table-active:not(.success-transition) {
+ transition: background-color 2s;
+}
+
+#subtitleTable tbody tr.success-transition {
+ background-color: #c3e6cb;
+}
+
+#episodeOrSeriesBtns label {
+ cursor: pointer;
+}
+
+#episodeOrSeriesBtns label.active {
+ cursor: default;
+}
+
+#progressModalTitle {
+ width: 100%;
+} \ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..bf65319
--- /dev/null
+++ b/index.html
@@ -0,0 +1,260 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>PASTA</title>
+
+ <!-- Required meta tags -->
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+
+ <!-- Bootstrap CSS -->
+ <link rel="stylesheet" type="text/css"
+ href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" />
+
+ <link rel="stylesheet" href="css/main.css" />
+ <!--<link rel="icon" type="image/png" href="images/favicon.png">-->
+
+ <!-- jQuery first, then Popper.js, then Bootstrap JS -->
+ <script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
+ integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
+ crossorigin="anonymous"></script>
+ <script type="text/javascript"
+ src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js"></script>
+ <!-- Custom Scripts -->
+ <script type="text/javascript" src="js/main.js"></script>
+</head>
+
+<body>
+ <!-- Navigation -->
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
+ <div class="container">
+ <a class="navbar-brand" href="#"><h3>PASTA<small class="text-muted ml-2">Plex Audio and Subtitle Track Automation</small></h3></a>
+ <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive"
+ aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <div class="collapse navbar-collapse" id="navbarResponsive">
+ <ul class="navbar-nav ml-auto">
+ <li class="nav-item active">
+ <a class="nav-link" href="#">Home
+ <span class="sr-only">(current)</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </nav>
+
+ <!-- Modal -->
+ <div class="modal fade" id="progressModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
+ <div class="modal-dialog modal-xl modal-dialog-centered" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title" id="progressModalTitle"></h5>
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div id="modalBodyText" class="modal-body">
+
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Page Content -->
+ <div class="container">
+ <div class="card border-0 shadow my-5">
+ <div class="card-body p-5">
+ <!-- PLEX LOGIN FORM -->
+ <div class="row">
+ <div class="col">
+ <div class="form-group">
+ <label for="plexUrl">Plex URL</label>
+ <input type="email" class="form-control" id="plexUrl" aria-describedby="emailHelp" placeholder="http://192.168.0.1:32400">
+ <small id="emailHelp" class="form-text text-muted">This must be a local server, or a server addressable by IP.</small>
+ </div>
+ <div class="form-group">
+ <label for="plexToken">Plex Token</label>
+ <input type="text" class="form-control" id="plexToken" placeholder="X-Plex-Token">
+ <small id="emailHelp" class="form-text text-muted">
+ <a target="_blank" href="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/">You can learn more here.</a>
+ </small>
+ </div>
+ <button class="btn btn-secondary" onclick="connectToPlex()">Connect to Plex</button>
+ </div>
+ </div>
+ <!-- / PLEX LOGIN FORM -->
+
+ <!-- LIBRARIES TABLE -->
+ <div class="row mt-5">
+ <div class="col">
+ <table id="libraryTable" class="table table-hover">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Name</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- / LIBRARIES TABLE -->
+
+ <!-- ALPHABET LIBRARY -->
+ <div class="row mt-5">
+ <div class="col text-center">
+ <div id="alphabetGroup" class="btn-group flex-wrap" role="group" aria-label="TV Library First Letter">
+ <!-- set class "disabled" for buttons which are not active -->
+ <button id="btnHash" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">#</button>
+ <button id="btnA" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">A</button>
+ <button id="btnB" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">B</button>
+ <button id="btnC" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">C</button>
+ <button id="btnD" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">D</button>
+ <button id="btnE" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">E</button>
+ <button id="btnF" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">F</button>
+ <button id="btnG" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">G</button>
+ <button id="btnH" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">H</button>
+ <button id="btnI" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">I</button>
+ <button id="btnJ" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">J</button>
+ <button id="btnK" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">K</button>
+ <button id="btnL" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">L</button>
+ <button id="btnM" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">M</button>
+ <button id="btnN" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">N</button>
+ <button id="btnO" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">O</button>
+ <button id="btnP" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">P</button>
+ <button id="btnQ" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">Q</button>
+ <button id="btnR" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">R</button>
+ <button id="btnS" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">S</button>
+ <button id="btnT" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">T</button>
+ <button id="btnU" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">U</button>
+ <button id="btnV" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">V</button>
+ <button id="btnW" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">W</button>
+ <button id="btnX" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">X</button>
+ <button id="btnY" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">Y</button>
+ <button id="btnZ" disabled type="button" class="btn btn-outline-dark" onclick="getLibraryByLetter(this)">Z</button>
+ </div>
+ </div>
+ </div>
+ <!-- / ALPHABET LIBRARY -->
+
+ <!-- SHOWS TABLE -->
+ <div class="row mt-5">
+ <div class="col">
+ <table id="tvShowsTable" class="table table-hover">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Title</th>
+ <th scope="col">Year</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- / SHOWS TABLE -->
+
+ <!-- SEASONS TABLE -->
+ <div class="row mt-5">
+ <div class="col">
+ <table id="seasonsTable" class="table table-hover">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- / SHOWS TABLE -->
+
+ <!-- EPISODES TABLE -->
+ <div class="row mt-5">
+ <div class="col">
+ <table id="episodesTable" class="table table-hover">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Title</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- / EPISODES TABLE -->
+
+ <!-- SWITCH TOGGLE -->
+ <div class="row mt-5">
+ <div class="col text-center">
+ <div id="episodeOrSeriesBtns" class="btn-group btn-group-toggle" data-toggle="buttons">
+ <label class="btn btn-secondary active">
+ <input type="radio" name="episodeOrSeries" id="singleEpisode" autocomplete="off" checked> Single Episode
+ </label>
+ <label class="btn btn-secondary">
+ <input type="radio" name="episodeOrSeries" id="entireSeries" autocomplete="off"> Entire Series
+ </label>
+ </div>
+ </div>
+ </div>
+ <!-- / SWITCH TOGGLE -->
+
+ <!-- STREAMS TABLES -->
+ <div class="row mt-5">
+ <div class="col">
+ <table id="audioTable" class="table table-hover table-sm">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Name</th>
+ <th scope="col">Title</th>
+ <th scope="col">Language</th>
+ <th scope="col">Code</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ <div class="col">
+ <table id="subtitleTable" class="table table-hover table-sm">
+ <thead>
+ <tr>
+ <th scope="col">UID</th>
+ <th scope="col">Name</th>
+ <th scope="col">Title</th>
+ <th scope="col">Language</th>
+ <th scope="col">Code</th>
+ </tr>
+ </thead>
+ <tbody>
+
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <!-- / STREAMS TABLES -->
+ </div>
+ </div>
+ </div>
+</body>
+
+</html> \ No newline at end of file
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..6fcd43c
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,593 @@
+var plexUrl;
+var plexToken;
+var libraryNumber = ""; // The Library ID that was clicked
+var showId = ""; // Stores the Id for the most recently clicked series
+var seasonsList = []; // Stores the Ids for all seasons of the most recently clicked series
+var seasonId = ""; // Store the Id of the most recently clicked season
+var episodeId = ""; // Stores the Id of the most recently clicked episode
+
+function connectToPlex() {
+ plexUrl = $("#plexUrl").val();
+ plexToken = $("#plexToken").val();
+
+ $.ajax({
+ "url": `${plexUrl}/library/sections/`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => displayLibraries(data),
+ "error": (data) => {
+ console.log("ERROR L22");
+ console.log(data);
+ }
+ });
+}
+
+function displayLibraries(data) {
+ const libraries = data.MediaContainer.Directory;
+ // console.log(libraries);
+
+ $("#tvShowsTable tbody").empty();
+ $("#seasonsTable tbody").empty();
+ $("#episodesTable tbody").empty();
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ for (let i = 0; i < libraries.length; i++) {
+ let rowHTML = `<tr onclick="getAlphabet(${libraries[i].key}, this)">
+ <th scope="row">${libraries[i].key}</th>
+ <td>${libraries[i].title}</td>
+ </tr>`;
+ $("#libraryTable tbody").append(rowHTML);
+ }
+}
+
+function getAlphabet(uid, row) {
+ $.ajax({
+ "url": `${plexUrl}/library/sections/${uid}/firstCharacter`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => {
+ libraryNumber = uid;
+ displayAlphabet(data, row);
+ },
+ "error": (data) => {
+ console.log("ERROR L60");
+ console.log(data);
+ }
+ });
+}
+
+function displayAlphabet(data, row) {
+ const availableAlphabet = data.MediaContainer.Directory;
+ // console.log(availableAlphabet);
+
+ $("#tvShowsTable tbody").empty();
+ $("#seasonsTable tbody").empty();
+ $("#episodesTable tbody").empty();
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+ $('#alphabetGroup').children().removeClass("btn-dark").addClass("btn-outline-dark").prop("disabled", true);
+
+ for (let i = 0; i < availableAlphabet.length; i++) {
+ if (availableAlphabet[i].title == "#") {
+ $(`#btnHash`).prop("disabled", false);
+ }
+ else {
+ $(`#btn${availableAlphabet[i].title}`).prop("disabled", false);
+ }
+ }
+}
+
+function getLibraryByLetter(element) {
+ let letter = $(element).text();
+ if (letter == "#") letter = "%23";
+ // console.log("getLibraryByLetter: " + letter);
+
+ $(element).siblings().removeClass("btn-dark").addClass("btn-outline-dark");
+ $(element).removeClass("btn-outline-dark").addClass("btn-dark");
+
+ $.ajax({
+ "url": `${plexUrl}/library/sections/${libraryNumber}/firstCharacter/${letter}`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => displayTitles(data),
+ "error": (data) => {
+ console.log("ERROR L107");
+ console.log(data);
+ }
+ });
+}
+
+function displayTitles(titles) {
+ const tvShows = titles.MediaContainer.Metadata;
+ // console.log(tvShows);
+ $("#tvShowsTable tbody").empty();
+ $("#seasonsTable tbody").empty();
+ $("#episodesTable tbody").empty();
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ for (let i = 0; i < tvShows.length; i++) {
+ let rowHTML = `<tr onclick="getTitleInfo(${tvShows[i].ratingKey}, this)">
+ <th scope="row">${tvShows[i].ratingKey}</th>
+ <td>${tvShows[i].title}</td>
+ <td>${tvShows[i].year}</td>
+ </tr>`;
+ $("#tvShowsTable tbody").append(rowHTML);
+ }
+}
+
+function getTitleInfo(uid, row) {
+ showId = uid;
+ $.ajax({
+ "url": `${plexUrl}/library/metadata/${uid}/children`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => showTitleInfo(data, row),
+ "error": (data) => {
+ console.log("ERROR L143");
+ console.log(data);
+ }
+ });
+}
+
+function showTitleInfo(data, row) {
+ const seasons = data.MediaContainer.Metadata;
+ seasonsList.length = 0;
+ // console.log(seasons);
+
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+
+ $("#seasonsTable tbody").empty();
+ $("#episodesTable tbody").empty();
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ for (let i = 0; i < seasons.length; i++) {
+ seasonsList.push(seasons[i].ratingKey);
+ let rowHTML = `<tr onclick="getSeasonInfo(${seasons[i].ratingKey}, this)">
+ <th scope="row">${seasons[i].ratingKey}</th>
+ <td>${seasons[i].title}</td>
+ </tr>`;
+ $("#seasonsTable tbody").append(rowHTML);
+ }
+}
+
+function getSeasonInfo(uid, row) {
+ seasonId = uid;
+ $.ajax({
+ "url": `${plexUrl}/library/metadata/${uid}/children`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => showSeasonInfo(data, row),
+ "error": (data) => {
+ console.log("ERROR L183");
+ console.log(data);
+ }
+ });
+}
+
+function showSeasonInfo(data, row) {
+ const episodes = data.MediaContainer.Metadata;
+ // console.log(episodes);
+
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+
+ $("#episodesTable tbody").empty();
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ for (let i = 0; i < episodes.length; i++) {
+ let rowHTML = `<tr onclick="getEpisodeInfo(${episodes[i].ratingKey}, this)">
+ <th scope="row">${episodes[i].ratingKey}</th>
+ <td>${episodes[i].title}</td>
+ </tr>`;
+ $("#episodesTable tbody").append(rowHTML);
+ }
+}
+
+function getEpisodeInfo(uid, row) {
+ episodeId = uid;
+ $.ajax({
+ "url": `${plexUrl}/library/metadata/${uid}`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => showEpisodeInfo(data, row),
+ "error": (data) => {
+ console.log("ERROR L220");
+ console.log(data);
+ }
+ });
+}
+
+function showEpisodeInfo(data, row) {
+ const streams = data.MediaContainer.Metadata[0].Media[0].Part[0].Stream;
+ const partId = data.MediaContainer.Metadata[0].Media[0].Part[0].id;
+ // console.log(partId);
+ // console.log(streams);
+
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+
+ $("#audioTable tbody").empty();
+ $("#subtitleTable tbody").empty();
+
+ for (let i = 0; i < streams.length; i++) {
+ if (streams[i].streamType == 2) {
+ let rowHTML = `<tr ${streams[i].selected ? "class='table-active'" : ""} onclick="setAudioStream(${partId}, ${streams[i].id}, this)">
+ <th class="uid" scope="row">${streams[i].id}</th>
+ <td class="name">${streams[i].displayTitle}</td>
+ <td class="title">${streams[i].title}</td>
+ <td class="language">${streams[i].language}</td>
+ <td class="code">${streams[i].languageCode}</td>
+ </tr>`;
+ $("#audioTable tbody").append(rowHTML);
+ }
+ else if (streams[i].streamType == 3) {
+ let rowHTML = `<tr ${streams[i].selected ? "class='table-active'" : ""} onclick="setSubtitleStream(${partId}, ${streams[i].id}, this)">
+ <th class="uid" scope="row">${streams[i].id}</th>
+ <td class="name">${streams[i].displayTitle}</td>
+ <td class="title">${streams[i].title}</td>
+ <td class="language">${streams[i].language}</td>
+ <td class="code">${streams[i].languageCode}</td>
+ </tr>`;
+ $("#subtitleTable tbody").append(rowHTML);
+ }
+ }
+}
+
+async function setAudioStream(partsId, streamId, row) {
+ let singleEpisode = $("#singleEpisode").prop("checked");
+
+ if (singleEpisode) {
+ //console.log("Apply Audio Stream to single episode");
+ $.ajax({
+ "url": `${plexUrl}/library/parts/${partsId}?audioStreamID=${streamId}&allParts=1`,
+ "method": "POST",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => {
+ //console.log("success");
+ $(row).siblings().removeClass("table-active").removeClass("table-success");
+ $(row).addClass("table-active").addClass("success-transition");
+ setTimeout(function() {
+ $(row).removeClass('success-transition');
+ }, 100);
+ },
+ "error": (data) => {
+ console.log("ERROR L283");
+ console.log(data);
+ }
+ });
+ }
+ else {
+ //console.log("Apply Audio Stream to whole series");
+ // Show the modal to set progress
+ $('#progressModal #progressModalTitle').empty();
+ $('#progressModal #progressModalTitle').text(`Processing Audio Changes`);
+ $('#progressModal #modalBodyText').empty();
+ $('#progressModal #modalBodyText').append(`<div class="alert alert-warning" role="alert">
+ <div class="d-flex align-items-center">
+ <span id="modalTitleText">Please do not close this tab or refresh until the process is complete</span>
+ <div class="spinner-border text-warning ml-auto" role="status" aria-hidden="true"></div>
+ </div>
+ </div>`);
+ $('#progressModal').modal();
+
+ let matchPromises = []; // This will store the promises to change the audio for given files. It means we can run in parallel and await them all
+ let searchTitle = $(".title", row).text();
+ let searchName = $(".name", row).text();
+ let searchLanguage = $(".language", row).text();
+ let searchCode = $(".code", row).text();
+
+ // We have the Seasons Ids stored in seasonsList, so iterate over them to get all the episodes
+ let episodeList = [];
+ for (let i = 0; i < seasonsList.length; i++) {
+ let seasonEpisodes = await $.ajax({
+ "url": `${plexUrl}/library/metadata/${seasonsList[i]}/children`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ }
+ });
+ for (let j = 0; j < seasonEpisodes.MediaContainer.Metadata.length; j++) {
+ episodeList.push(seasonEpisodes.MediaContainer.Metadata[j].ratingKey);
+ }
+ }
+ //console.log(episodeList);
+
+ // We have the episodes in episodeList, now we need to go through each one and see what streams are available
+ for (let i = 0; i < episodeList.length; i++) {
+ let episodeData = await $.ajax({
+ "url": `${plexUrl}/library/metadata/${episodeList[i]}`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ }
+ });
+ const episodePartId = episodeData.MediaContainer.Metadata[0].Media[0].Part[0].id;
+ const episodeStreams = episodeData.MediaContainer.Metadata[0].Media[0].Part[0].Stream;
+ //console.log(episodePartId);
+ //console.log(episodeStreams);
+
+ // Loop through each audio stream and check for any matches using the searchTitle, searchName, searchLanguage, searchCode
+ let hasMatch = false;
+ let newStreamId = "";
+ let matchType = "";
+
+ for (let j = 0; j < episodeStreams.length; j++) {
+ // Audio streams are streamType 2, so we only care about that, we also don't care if the track is already selected
+ if ((episodeStreams[j].streamType == "2") && (episodeStreams[j].selected == true)) {
+
+ }
+ else if (episodeStreams[j].streamType == "2") {
+ // If the displayTitle and title are the same, we have an instant match (also rule out any undefined matches)
+ if ((episodeStreams[j].title == searchTitle) && (episodeStreams[j].displayTitle == searchName) && (episodeStreams[j].title != "undefined") && (episodeStreams[j].displayTitle != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Name and Title";
+ break;
+ }
+ // If the titles are the same (rule out undefined match)
+ else if ((episodeStreams[j].title == searchTitle) && (episodeStreams[j].title != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Title";
+ break;
+ }
+ // If the names are the same (rule out undefined match)
+ else if ((episodeStreams[j].displayTitle == searchName) && (episodeStreams[j].displayTitle != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Name";
+ break;
+ }
+ // If the languages are the same (rule out undefined match)
+ else if ((episodeStreams[j].language == searchLanguage) && (episodeStreams[j].language != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Language";
+ break;
+ }
+ // If the language codes are the same (rule out undefined match)
+ else if ((episodeStreams[j].languageCode == searchCode) && (episodeStreams[j].languageCode != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Language Code";
+ break;
+ }
+ }
+ }
+
+ if (hasMatch) {
+ // There is a match, so update the audio track using the newStreamId and episodePartId
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} has a match on the type: ${matchType}, and will set the new Audio Track to: ${newStreamId}`);
+ matchPromises.push(await $.ajax({
+ "url": `${plexUrl}/library/parts/${episodePartId}?audioStreamID=${newStreamId}&allParts=1`,
+ "method": "POST",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => {
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} updated with Audio Track: ${newStreamId} because of a match on ${matchType}`);
+ $('#progressModal #modalBodyText').append(`<span><strong>${episodeData.MediaContainer.Metadata[0].title}</strong> updated with Audio Track: <strong>${newStreamId}</strong> because of a match on <strong>${matchType}</strong></span><br />`);
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+ },
+ "error": (data) => {
+ console.log("ERROR L406");
+ console.log(data);
+ }
+ }));
+ }
+ else {
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} has no match, or there is only 1 audio track`);
+ }
+ }
+ try {
+ await Promise.all(matchPromises.map(p => p.catch(e => e)));
+ }
+ catch (e) {
+ console.log("ERROR L419");
+ console.log(e);
+ }
+ //console.log("Completed all Updates");
+ $('#modalBodyText .alert').removeClass("alert-warning").addClass("alert-success");
+ $("#modalBodyText #modalTitleText").text("Processing Complete!");
+ $('#modalBodyText .spinner-border').css('visibility','hidden');
+ }
+}
+
+async function setSubtitleStream(partsId, streamId, row) {
+ let singleEpisode = $("#singleEpisode").prop("checked");
+
+ if (singleEpisode) {
+ $.ajax({
+ "url": `${plexUrl}/library/parts/${partsId}?subtitleStreamID=${streamId}&allParts=1`,
+ "method": "POST",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => {
+ //console.log("success");
+ $(row).siblings().removeClass("table-active").removeClass("table-success");
+ $(row).addClass("table-active").addClass("success-transition");
+ setTimeout(function() {
+ $(row).removeClass('success-transition');
+ }, 100);
+ },
+ "error": (data) => {
+ console.log("ERROR L449");
+ console.log(data);
+ }
+ });
+ }
+ else {
+ //console.log("Apply Subtitle Stream to whole series");
+ // Show the modal to set progress
+ $('#progressModal #progressModalTitle').empty();
+ $('#progressModal #progressModalTitle').text(`Processing Subtitle Changes`);
+ $('#progressModal #modalBodyText').empty();
+ $('#progressModal #modalBodyText').append(`<div class="alert alert-warning" role="alert">
+ <div class="d-flex align-items-center">
+ <span id="modalTitleText">Please do not close this tab or refresh until the process is complete</span>
+ <div class="spinner-border text-warning ml-auto" role="status" aria-hidden="true"></div>
+ </div>
+ </div>`);
+ $('#progressModal').modal();
+
+ let matchPromises = []; // This will store the promises to change the audio for given files. It means we can run in parallel and await them all
+ let searchTitle = $(".title", row).text();
+ let searchName = $(".name", row).text();
+ let searchLanguage = $(".language", row).text();
+ let searchCode = $(".code", row).text();
+
+ // We have the Seasons Ids stored in seasonsList, so iterate over them to get all the episodes
+ let episodeList = [];
+ for (let i = 0; i < seasonsList.length; i++) {
+ let seasonEpisodes = await $.ajax({
+ "url": `${plexUrl}/library/metadata/${seasonsList[i]}/children`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ }
+ });
+ for (let j = 0; j < seasonEpisodes.MediaContainer.Metadata.length; j++) {
+ episodeList.push(seasonEpisodes.MediaContainer.Metadata[j].ratingKey);
+ }
+ }
+ //console.log(episodeList);
+
+ // We have the episodes in episodeList, now we need to go through each one and see what streams are available
+ for (let i = 0; i < episodeList.length; i++) {
+ let episodeData = await $.ajax({
+ "url": `${plexUrl}/library/metadata/${episodeList[i]}`,
+ "method": "GET",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ }
+ });
+ const episodePartId = episodeData.MediaContainer.Metadata[0].Media[0].Part[0].id;
+ const episodeStreams = episodeData.MediaContainer.Metadata[0].Media[0].Part[0].Stream;
+ //console.log(episodePartId);
+ //console.log(episodeStreams);
+
+ // Loop through each subtitle stream and check for any matches using the searchTitle, searchName, searchLanguage, searchCode
+ let hasMatch = false;
+ let newStreamId = "";
+ let matchType = "";
+
+ for (let j = 0; j < episodeStreams.length; j++) {
+ // Sudio streams are streamType 3, so we only care about that, we also don't care if the track is already selected
+ if ((episodeStreams[j].streamType == "3") && (episodeStreams[j].selected == true)) {
+
+ }
+ else if (episodeStreams[j].streamType == "3") {
+ // If the displayTitle and title are the same, we have an instant match (also rule out any undefined matches)
+ if ((episodeStreams[j].title == searchTitle) && (episodeStreams[j].displayTitle == searchName) && (episodeStreams[j].title != "undefined") && (episodeStreams[j].displayTitle != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Name and Title";
+ break;
+ }
+ // If the titles are the same (rule out undefined match)
+ else if ((episodeStreams[j].title == searchTitle) && (episodeStreams[j].title != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Title";
+ break;
+ }
+ // If the names are the same (rule out undefined match)
+ else if ((episodeStreams[j].displayTitle == searchName) && (episodeStreams[j].displayTitle != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Name";
+ break;
+ }
+ // If the languages are the same (rule out undefined match)
+ else if ((episodeStreams[j].language == searchLanguage) && (episodeStreams[j].language != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Language";
+ break;
+ }
+ // If the language codes are the same (rule out undefined match)
+ else if ((episodeStreams[j].languageCode == searchCode) && (episodeStreams[j].languageCode != "undefined")) {
+ hasMatch = true;
+ newStreamId = episodeStreams[j].id;
+ matchType = "Language Code";
+ break;
+ }
+ }
+ }
+
+ if (hasMatch) {
+ // There is a match, so update the subtitle track using the newStreamId and episodePartId
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} has a match on the type: ${matchType}, and will set the new Subtitle Track to: ${newStreamId}`);
+ matchPromises.push(await $.ajax({
+ "url": `${plexUrl}/library/parts/${episodePartId}?subtitleStreamID=${newStreamId}&allParts=1`,
+ "method": "POST",
+ "headers": {
+ "X-Plex-Token": plexToken,
+ "Accept": "application/json"
+ },
+ "success": (data) => {
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} updated with Subtitle Track: ${newStreamId} because of a match on ${matchType}`);
+ $('#progressModal #modalBodyText').append(`<span><strong>${episodeData.MediaContainer.Metadata[0].title}</strong> updated with Subtitle Track: <strong>${newStreamId}</strong> because of a match on <strong>${matchType}</strong></span><br />`);
+ $(row).siblings().removeClass("table-active");
+ $(row).addClass("table-active");
+ },
+ "error": (data) => {
+ console.log("ERROR L572");
+ console.log(data);
+ }
+ }));
+ }
+ else {
+ //console.log(`Episode: ${episodeData.MediaContainer.Metadata[0].title} has no match, or there is only 1 subtitle track`);
+ }
+ }
+ try {
+ await Promise.all(matchPromises.map(p => p.catch(e => e)));
+ }
+ catch (e) {
+ console.log("ERROR 585");
+ console.log(e);
+ }
+ //console.log("Completed all Updates");
+ $('#modalBodyText .alert').removeClass("alert-warning").addClass("alert-success");
+ $("#modalBodyText #modalTitleText").text("Processing Complete!");
+ $('#modalBodyText .spinner-border').css('visibility','hidden');
+ }
+} \ No newline at end of file