Duncan Tourolle b4275837bc
All checks were successful
🏗️ Build Plugin / build (push) Successful in 2m4s
🧪 Test Plugin / test (push) Successful in 1m1s
🚀 Release Plugin / build-and-release (push) Successful in 2m39s
Updated readme
fixed rending error on pod management page
2025-12-14 14:25:43 +01:00

417 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Jellypod</title>
<style>
.podcast-table {
width: 100%;
border-collapse: collapse;
}
.podcast-table th {
text-align: left;
padding: 0.75em;
border-bottom: 2px solid rgba(255,255,255,0.2);
font-weight: 600;
}
.podcast-table td {
padding: 0.75em;
border-bottom: 1px solid rgba(255,255,255,0.1);
vertical-align: middle;
}
.podcast-table tr:last-child td {
border-bottom: none;
}
.podcast-table tr:hover {
background: rgba(255,255,255,0.05);
}
.podcast-image {
width: 50px !important;
height: 50px !important;
max-width: 50px !important;
max-height: 50px !important;
min-width: 50px !important;
min-height: 50px !important;
object-fit: cover !important;
border-radius: 4px !important;
background: #333 !important;
display: block !important;
}
.col-image img {
width: 50px !important;
height: 50px !important;
max-width: 50px !important;
max-height: 50px !important;
}
.podcast-title {
font-weight: bold;
}
.podcast-meta {
font-size: 0.85em;
opacity: 0.7;
margin-top: 0.25em;
}
.podcast-actions {
display: flex;
gap: 0.5em;
justify-content: flex-end;
}
.col-image {
width: 50px;
}
.col-actions {
width: 100px;
text-align: right;
}
.add-podcast-form {
display: flex;
gap: 0.5em;
align-items: flex-end;
margin-bottom: 1em;
}
.add-podcast-form .inputContainer {
flex: 1;
margin: 0;
}
.section-divider {
margin: 2em 0;
border-top: 1px solid rgba(255,255,255,0.2);
}
.empty-state {
text-align: center;
padding: 2em;
opacity: 0.7;
}
.info-box {
background: rgba(0,100,200,0.2);
border: 1px solid rgba(0,100,200,0.4);
border-radius: 4px;
padding: 1em;
margin-bottom: 1em;
}
</style>
</head>
<body>
<div id="JellypodConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<div data-role="content">
<div class="content-primary">
<h2 class="sectionTitle">Jellypod Settings</h2>
<div class="info-box">
<strong>Browse Podcasts:</strong> Your subscribed podcasts appear in the <em>Channels</em> section of Jellyfin's main menu.
Use this settings page to add/remove podcast subscriptions and configure download options.
</div>
<form id="JellypodConfigForm">
<!-- Storage Settings -->
<div class="verticalSection">
<h3 class="sectionTitle">Storage</h3>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="PodcastStoragePath">
Download Storage Path
</label>
<input id="PodcastStoragePath" name="PodcastStoragePath" type="text" is="emby-input" />
<div class="fieldDescription">
Path where downloaded episodes are stored. Leave empty for default location.
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="CreatePodcastFolders" name="CreatePodcastFolders" type="checkbox" is="emby-checkbox" />
<span>Create subfolders for each podcast</span>
</label>
</div>
</div>
<!-- Update Settings -->
<div class="verticalSection">
<h3 class="sectionTitle">Feed Updates</h3>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="UpdateIntervalHours">
Update Interval (hours)
</label>
<input id="UpdateIntervalHours" name="UpdateIntervalHours" type="number" is="emby-input" min="1" max="168" />
<div class="fieldDescription">
How often to check for new episodes (default: 6 hours)
</div>
</div>
</div>
<!-- Download Settings -->
<div class="verticalSection">
<h3 class="sectionTitle">Downloads</h3>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="GlobalAutoDownloadEnabled" name="GlobalAutoDownloadEnabled" type="checkbox" is="emby-checkbox" />
<span>Automatically download new episodes</span>
</label>
<div class="fieldDescription">
Episodes can always be streamed directly. Enable this to also download them locally.
</div>
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxConcurrentDownloads">
Max Concurrent Downloads
</label>
<input id="MaxConcurrentDownloads" name="MaxConcurrentDownloads" type="number" is="emby-input" min="1" max="5" />
</div>
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="MaxEpisodesPerPodcast">
Max Episodes Per Podcast
</label>
<input id="MaxEpisodesPerPodcast" name="MaxEpisodesPerPodcast" type="number" is="emby-input" min="0" />
<div class="fieldDescription">
Maximum episodes to keep downloaded per podcast (0 = unlimited)
</div>
</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block emby-button">
<span>Save Settings</span>
</button>
</div>
</form>
<div class="section-divider"></div>
<!-- Podcast Subscription Management -->
<div class="verticalSection">
<h2 class="sectionTitle">Podcast Subscriptions</h2>
<!-- Add Podcast Form -->
<div class="add-podcast-form">
<div class="inputContainer">
<label class="inputLabel inputLabelUnfocused" for="NewFeedUrl">
RSS Feed URL
</label>
<input id="NewFeedUrl" type="url" is="emby-input" placeholder="https://example.com/feed.xml" />
</div>
<button is="emby-button" type="button" id="btnAddPodcast" class="raised emby-button">
<span>Subscribe</span>
</button>
</div>
<!-- Podcast List -->
<div id="podcastList">
<div class="empty-state" id="emptyState">
No podcast subscriptions yet. Add one above, then browse in Channels.
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
var JellypodConfig = {
pluginUniqueId: 'c713faf4-4e50-4e87-941a-1200178ed605'
};
function loadConfig() {
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(JellypodConfig.pluginUniqueId).then(function (config) {
document.querySelector('#PodcastStoragePath').value = config.PodcastStoragePath || '';
document.querySelector('#UpdateIntervalHours').value = config.UpdateIntervalHours;
document.querySelector('#GlobalAutoDownloadEnabled').checked = config.GlobalAutoDownloadEnabled;
document.querySelector('#MaxConcurrentDownloads').value = config.MaxConcurrentDownloads;
document.querySelector('#MaxEpisodesPerPodcast').value = config.MaxEpisodesPerPodcast;
document.querySelector('#CreatePodcastFolders').checked = config.CreatePodcastFolders;
Dashboard.hideLoadingMsg();
});
}
function loadPodcasts() {
console.log('Jellypod: Loading podcasts...');
ApiClient.fetch({
url: ApiClient.getUrl('Jellypod/podcasts'),
type: 'GET'
}).then(function(response) {
// ApiClient.fetch returns a Response object, need to parse JSON
return response.json();
}).then(function(podcasts) {
console.log('Jellypod: Received podcasts:', podcasts);
renderPodcasts(podcasts);
}).catch(function(err) {
console.error('Jellypod: Failed to load podcasts:', err);
renderPodcasts([]);
});
}
function renderPodcasts(podcasts) {
var container = document.querySelector('#podcastList');
console.log('Jellypod: Rendering podcasts, count:', podcasts ? podcasts.length : 0);
if (!podcasts || podcasts.length === 0) {
container.innerHTML = '<div class="empty-state">No podcast subscriptions yet. Add one above, then browse in Channels.</div>';
return;
}
var rows = podcasts.map(function(podcast) {
// Handle both PascalCase (C#) and camelCase (JSON) property names
var episodeCount = (podcast.Episodes || podcast.episodes || []).length;
var lastUpdated = podcast.LastUpdated || podcast.lastUpdated;
var lastUpdatedStr = lastUpdated ? new Date(lastUpdated).toLocaleString() : 'Never';
var podcastId = podcast.Id || podcast.id;
var podcastTitle = podcast.Title || podcast.title || 'Unknown';
var podcastImage = podcast.ImageUrl || podcast.imageUrl || '';
console.log('Jellypod: Rendering podcast:', podcastTitle, 'ID:', podcastId);
return '<tr data-id="' + podcastId + '">' +
'<td class="col-image">' +
'<img class="podcast-image" src="' + podcastImage + '" alt="" style="width:50px;height:50px;max-width:50px;max-height:50px;object-fit:cover;" onerror="this.style.display=\'none\'">' +
'</td>' +
'<td>' +
'<div class="podcast-title">' + escapeHtml(podcastTitle) + '</div>' +
'<div class="podcast-meta">' + episodeCount + ' episodes | Updated: ' + lastUpdatedStr + '</div>' +
'</td>' +
'<td class="col-actions">' +
'<div class="podcast-actions">' +
'<button is="emby-button" type="button" class="emby-button" onclick="refreshPodcast(\'' + podcastId + '\')" title="Refresh Feed">' +
'<span class="material-icons">refresh</span>' +
'</button>' +
'<button is="emby-button" type="button" class="emby-button" onclick="deletePodcast(\'' + podcastId + '\')" title="Unsubscribe">' +
'<span class="material-icons">delete</span>' +
'</button>' +
'</div>' +
'</td>' +
'</tr>';
}).join('');
var html = '<table class="podcast-table">' +
'<thead><tr>' +
'<th class="col-image"></th>' +
'<th>Podcast</th>' +
'<th class="col-actions">Actions</th>' +
'</tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>';
container.innerHTML = html;
console.log('Jellypod: Rendered HTML length:', html.length);
}
function escapeHtml(text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function addPodcast() {
var feedUrl = document.querySelector('#NewFeedUrl').value.trim();
if (!feedUrl) {
Dashboard.alert('Please enter a feed URL');
return;
}
Dashboard.showLoadingMsg();
ApiClient.fetch({
url: ApiClient.getUrl('Jellypod/podcasts'),
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ feedUrl: feedUrl })
}).then(function(podcast) {
document.querySelector('#NewFeedUrl').value = '';
Dashboard.hideLoadingMsg();
loadPodcasts();
Dashboard.alert('Subscribed to: ' + podcast.title + '\n\nBrowse episodes in Channels > Podcasts');
}).catch(function(err) {
Dashboard.hideLoadingMsg();
Dashboard.alert('Failed to subscribe. Please check the URL and try again.');
});
}
window.refreshPodcast = function(id) {
Dashboard.showLoadingMsg();
ApiClient.fetch({
url: ApiClient.getUrl('Jellypod/podcasts/' + id + '/refresh'),
type: 'POST'
}).then(function() {
Dashboard.hideLoadingMsg();
loadPodcasts();
Dashboard.alert('Feed refreshed');
}).catch(function(err) {
Dashboard.hideLoadingMsg();
Dashboard.alert('Failed to refresh feed');
});
};
window.deletePodcast = function(id) {
console.log('Jellypod: deletePodcast called with id:', id);
// Use simple confirm since require(['confirm']) may not work in newer Jellyfin
if (confirm('Are you sure you want to unsubscribe from this podcast?')) {
console.log('Jellypod: User confirmed deletion');
Dashboard.showLoadingMsg();
ApiClient.fetch({
url: ApiClient.getUrl('Jellypod/podcasts/' + id + '?deleteFiles=true'),
type: 'DELETE'
}).then(function() {
console.log('Jellypod: Delete successful');
Dashboard.hideLoadingMsg();
loadPodcasts();
}).catch(function(err) {
console.error('Jellypod: Delete failed:', err);
Dashboard.hideLoadingMsg();
Dashboard.alert('Failed to unsubscribe');
});
}
};
// Jellyfin uses 'viewshow' event for SPA navigation, not 'pageshow'
document.querySelector('#JellypodConfigPage').addEventListener('viewshow', function() {
console.log('Jellypod: viewshow event fired');
loadConfig();
loadPodcasts();
});
// Also handle pageshow as fallback
document.querySelector('#JellypodConfigPage').addEventListener('pageshow', function() {
console.log('Jellypod: pageshow event fired');
loadConfig();
loadPodcasts();
});
// Initialize immediately if the page is already visible (handles direct page load)
(function() {
console.log('Jellypod: Script loaded, checking if should initialize...');
// Small delay to ensure Jellyfin's framework is ready
setTimeout(function() {
var page = document.querySelector('#JellypodConfigPage');
if (page && page.offsetParent !== null) {
console.log('Jellypod: Page visible, initializing...');
loadConfig();
loadPodcasts();
}
}, 100);
})();
document.querySelector('#JellypodConfigForm').addEventListener('submit', function(e) {
e.preventDefault();
Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(JellypodConfig.pluginUniqueId).then(function (config) {
config.PodcastStoragePath = document.querySelector('#PodcastStoragePath').value;
config.UpdateIntervalHours = parseInt(document.querySelector('#UpdateIntervalHours').value, 10);
config.GlobalAutoDownloadEnabled = document.querySelector('#GlobalAutoDownloadEnabled').checked;
config.MaxConcurrentDownloads = parseInt(document.querySelector('#MaxConcurrentDownloads').value, 10);
config.MaxEpisodesPerPodcast = parseInt(document.querySelector('#MaxEpisodesPerPodcast').value, 10);
config.CreatePodcastFolders = document.querySelector('#CreatePodcastFolders').checked;
ApiClient.updatePluginConfiguration(JellypodConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
});
});
return false;
});
document.querySelector('#btnAddPodcast').addEventListener('click', addPodcast);
document.querySelector('#NewFeedUrl').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addPodcast();
}
});
</script>
</div>
</body>
</html>