diff options
author | Ciaran Gallagher <[email protected]> | 2019-11-28 18:06:15 +0000 |
---|---|---|
committer | Ciaran Gallagher <[email protected]> | 2019-11-28 18:06:15 +0000 |
commit | e118c68d12c82e4e7fda20244dda05c7f8f4bb79 (patch) | |
tree | 48b10d85821539cc9d356872cf06d1a98124b565 | |
parent | add7b7e8ebc881e3e92e03af46ff08059a7687da (diff) | |
download | pasta-e118c68d12c82e4e7fda20244dda05c7f8f4bb79.tar.gz pasta-e118c68d12c82e4e7fda20244dda05c7f8f4bb79.zip |
Initial Commit
-rw-r--r-- | css/main.css | 51 | ||||
-rw-r--r-- | index.html | 260 | ||||
-rw-r--r-- | js/main.js | 593 |
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">×</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 |