Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ <h5 class="modal-title" id="progressModalTitle"></h5>
</div>

<!-- Toast Confirmation Message -->
<div class="toast" id="successToast" role="alert" aria-live="assertive" aria-atomic="true" style="">
<div class="toast" id="successToast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body alert-success">
Success!
</div>
Expand Down Expand Up @@ -412,6 +412,12 @@ <h3>Plex Libraries</h3>
<div class="row mt-4">
<div class="col">
<h3 id="libraryTypeTitle">TV Series</h3>
<div class="input-group mb-3">
<input type="text" id="showSearchInput" class="form-control" placeholder="Search for a show..." aria-label="Search for a show">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="clearShowSearch">Clear</button>
</div>
</div>
<div class="table-responsive">
<table id="tvShowsTable" class="table table-hover mt-3">
<thead>
Expand All @@ -421,7 +427,6 @@ <h3 id="libraryTypeTitle">TV Series</h3>
</tr>
</thead>
<tbody>

</tbody>
</table>
</div>
Expand Down Expand Up @@ -521,6 +526,11 @@ <h3>Audio Tracks</h3>
</div>
<div class="col">
<h3>Subtitle Tracks</h3>
<div class="form-group mb-2">
<label for="subtitleKeywordInput"><strong>Subtitle Keyword</strong></label>
<input type="text" id="subtitleKeywordInput" class="form-control" placeholder="Enter keyword to match subtitles">
<small class="form-text text-muted">Only subtitles containing this keyword will be matched.</small>
</div>
<div class="table-responsive">
<table id="subtitleTable" class="table table-hover table-sm mt-3">
<thead>
Expand Down
116 changes: 109 additions & 7 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ function displayLibraries(data) {
$("#subtitleTable tbody").empty();

for (let i = 0; i < libraries.length; i++) {
let rowHTML = `<tr onclick="getAlphabet(${libraries[i].key}, '${libraries[i].type}', this)">
let rowHTML = `<tr onclick="getAlphabet(${libraries[i].key}, '${libraries[i].type}', this); setupShowSearch(${libraries[i].key}, '${libraries[i].type}')">
<td>${libraries[i].title}</td>
</tr>`;
$("#libraryTable tbody").append(rowHTML);
Expand All @@ -523,6 +523,50 @@ function displayLibraries(data) {
});
}

function setupShowSearch(libraryKey, libType) {
$(document).off('input', '#showSearchInput');
$(document).off('click', '#clearShowSearch');
window.showSearchSetup = true;
let allShowsCache = null;
async function fetchAllShows() {
if (allShowsCache) return allShowsCache;
try {
const result = await $.ajax({
"url": `${plexUrl}/library/sections/${libraryKey}/all`,
"method": "GET",
"headers": {
"X-Plex-Token": plexToken,
"Accept": "application/json"
}
});
allShowsCache = result.MediaContainer.Metadata || [];
return allShowsCache;
} catch (e) {
return [];
}
}
$(document).on('input', '#showSearchInput', async function() {
$('#alphabetGroup .btn').removeClass('btn-dark').addClass('btn-outline-dark');
const search = $(this).val().toLowerCase();
if (search.length === 0) {
$("#tvShowsTable tbody").empty();
return;
}
const allShows = await fetchAllShows();
const filtered = allShows.filter(show => show.title && show.title.toLowerCase().includes(search));
$("#tvShowsTable tbody").empty();
for (let i = 0; i < filtered.length; i++) {
let rowHTML = `<tr onclick=\"getTitleInfo(${filtered[i].ratingKey}, this)\">\n<td>${filtered[i].title}</td>\n<td>${filtered[i].year}</td>\n</tr>`;
$("#tvShowsTable tbody").append(rowHTML);
}
});
$(document).on('click', '#clearShowSearch', function() {
$('#showSearchInput').val('');
$('#alphabetGroup .btn').removeClass('btn-dark').addClass('btn-outline-dark');
$("#tvShowsTable tbody").empty();
});
}

function getAlphabet(uid, libType, row) {
$.ajax({
"url": `${plexUrl}/library/sections/${uid}/firstCharacter`,
Expand Down Expand Up @@ -604,6 +648,8 @@ function getLibraryByLetter(element) {
let letter = $(element).text();
if (letter == "#") letter = "%23";

$('#showSearchInput').val('');

$(element).siblings().removeClass("btn-dark").addClass("btn-outline-dark");
$(element).removeClass("btn-outline-dark").addClass("btn-dark");

Expand All @@ -630,13 +676,60 @@ function displayTitles(titles) {
$("#audioTable tbody").empty();
$("#subtitleTable tbody").empty();

for (let i = 0; i < tvShows.length; i++) {
let rowHTML = `<tr onclick="getTitleInfo(${tvShows[i].ratingKey}, this)">
<td>${tvShows[i].title}</td>
<td>${tvShows[i].year}</td>
</tr>`;
$("#tvShowsTable tbody").append(rowHTML);
// Store the last loaded shows for search filtering
window.lastLoadedShows = tvShows || [];

function renderShowsTable(shows) {
$("#tvShowsTable tbody").empty();
for (let i = 0; i < shows.length; i++) {
let rowHTML = `<tr onclick=\"getTitleInfo(${shows[i].ratingKey}, this)\">
<td>${shows[i].title}</td>
<td>${shows[i].year}</td>
</tr>`;
$("#tvShowsTable tbody").append(rowHTML);
}
}

renderShowsTable(tvShows);

if (!window.showSearchSetup) {
window.showSearchSetup = true;
let allShowsCache = null;
async function fetchAllShows() {
if (allShowsCache) return allShowsCache;
// Fetch all shows in the current library
try {
const result = await $.ajax({
"url": `${plexUrl}/library/sections/${libraryNumber}/all`,
"method": "GET",
"headers": {
"X-Plex-Token": plexToken,
"Accept": "application/json"
}
});
allShowsCache = result.MediaContainer.Metadata || [];
return allShowsCache;
} catch (e) {
return [];
}
}

$(document).on('input', '#showSearchInput', async function() {
const search = $(this).val().toLowerCase();
if (search.length === 0) {
renderShowsTable(window.lastLoadedShows || []);
return;
}
const allShows = await fetchAllShows();
const filtered = allShows.filter(show => show.title && show.title.toLowerCase().includes(search));
renderShowsTable(filtered);
});
$(document).on('click', '#clearShowSearch', function() {
$('#showSearchInput').val('');
renderShowsTable(window.lastLoadedShows || []);
});
}

// Scroll to the table
document.querySelector('#tvShowsTable').scrollIntoView({
behavior: 'smooth'
Expand Down Expand Up @@ -1238,6 +1331,9 @@ async function setSubtitleStream(partsId, streamId, row) {

// If streamId = 0 then we are unsetting the subtitles. Otherwise we need to find the best matches for each episode
if (streamId != 0) {
// Get the subtitle keyword from the UI
let subtitleKeyword = $('#subtitleKeywordInput').val().trim().toLowerCase();

// Loop through each subtitle stream and check for any matches using the searchTitle, searchName, searchLanguage, searchCode
let hasMatch = false;
let matchType = "";
Expand All @@ -1252,6 +1348,12 @@ async function setSubtitleStream(partsId, streamId, row) {
for (let j = 0; j < episodeStreams.length; j++) {
// Subtitle streams are streamType 3, so we only care about that
if (episodeStreams[j].streamType == "3") {
// If a keyword is set, skip this subtitle if it doesn't match
if (subtitleKeyword &&
!(String(episodeStreams[j].title || '').toLowerCase().includes(subtitleKeyword) ||
String(episodeStreams[j].displayTitle || '').toLowerCase().includes(subtitleKeyword))) {
continue;
}
// If EVERYTHING is a match, even if they are "undefined" then select it
if ((episodeStreams[j].title == searchTitle) && (episodeStreams[j].displayTitle == searchName) && (episodeStreams[j].language == searchLanguage) && (episodeStreams[j].languageCode == searchCode)) {
if (episodeStreams[j].selected == true) {
Expand Down