384 lines
18 KiB
HTML
384 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Jellypod</title>
|
|
<style>
|
|
.podcast-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75em;
|
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
.podcast-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.podcast-image {
|
|
width: 50px;
|
|
height: 50px;
|
|
max-width: 50px;
|
|
max-height: 50px;
|
|
min-width: 50px;
|
|
min-height: 50px;
|
|
object-fit: cover;
|
|
border-radius: 4px;
|
|
margin-right: 1em;
|
|
background: #333;
|
|
flex-shrink: 0;
|
|
}
|
|
.podcast-info {
|
|
flex: 1;
|
|
}
|
|
.podcast-title {
|
|
font-weight: bold;
|
|
margin-bottom: 0.25em;
|
|
}
|
|
.podcast-meta {
|
|
font-size: 0.85em;
|
|
opacity: 0.7;
|
|
}
|
|
.podcast-actions {
|
|
display: flex;
|
|
gap: 0.5em;
|
|
}
|
|
.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 html = 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 '<div class="podcast-item" data-id="' + podcastId + '">' +
|
|
'<img class="podcast-image" src="' + podcastImage + '" alt="" onerror="this.style.display=\'none\'">' +
|
|
'<div class="podcast-info">' +
|
|
'<div class="podcast-title">' + escapeHtml(podcastTitle) + '</div>' +
|
|
'<div class="podcast-meta">' +
|
|
episodeCount + ' episodes | Updated: ' + lastUpdatedStr +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<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>' +
|
|
'</div>';
|
|
}).join('');
|
|
|
|
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>
|