Updates
This commit is contained in:
parent
e86ab53de5
commit
095bf52a2f
29 changed files with 2494 additions and 758 deletions
|
@ -8,33 +8,70 @@
|
|||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-container">
|
||||
<!-- Header with app name and search -->
|
||||
<header class="main-header">
|
||||
<div class="logo">
|
||||
<div class="app-container">
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h1>Podcastrr</h1>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<form action="{{ url_for('podcasts.search') }}" method="post" class="header-search-form">
|
||||
<input type="text" name="query" placeholder="Search podcasts...">
|
||||
<button type="submit" class="btn btn-search">Search</button>
|
||||
</form>
|
||||
<div class="sidebar-nav">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('main.index') }}"
|
||||
class="{% if request.endpoint == 'main.index' %}active{% endif %}">
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('podcasts.index') }}"
|
||||
class="{% if request.endpoint in ['podcasts.index', 'podcasts.view'] %}active{% endif %}">
|
||||
Podcasts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('podcasts.search') }}"
|
||||
class="{% if request.endpoint == 'podcasts.search' %}active{% endif %}">
|
||||
Add New
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('main.dashboard') }}"
|
||||
class="{% if request.endpoint == 'main.dashboard' %}active{% endif %}">
|
||||
Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('settings.index') }}"
|
||||
class="{% if request.endpoint == 'settings.index' %}active{% endif %}">
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Task Status Area -->
|
||||
<div id="task-status-area" style="display: none; margin-top: auto; padding: 10px; background-color: #161b22; border-top: 1px solid #30363d;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<h3 style="margin: 0; font-size: 14px; color: #f0f6fc;">Current Tasks</h3>
|
||||
<div style="margin-left: auto;">
|
||||
<button id="close-tasks-btn" class="btn btn-sm" style="padding: 2px 8px;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tasks-container"></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Left sidebar navigation -->
|
||||
<nav class="sidebar">
|
||||
<ul class="sidebar-nav">
|
||||
<li><a href="{{ url_for('main.index') }}">Home</a></li>
|
||||
<li><a href="{{ url_for('podcasts.index') }}">Podcasts</a></li>
|
||||
<li><a href="{{ url_for('main.dashboard') }}">Dashboard</a></li>
|
||||
<li><a href="{{ url_for('settings.index') }}">Settings</a></li>
|
||||
<li><a href="{{ url_for('main.about') }}">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Main content area -->
|
||||
<main class="main-content">
|
||||
<!-- Main Area -->
|
||||
<div class="main-area">
|
||||
<!-- Top Header -->
|
||||
<header class="top-header">
|
||||
<div class="header-search">
|
||||
<form action="{{ url_for('podcasts.search') }}" method="post">
|
||||
<input type="text" name="query" placeholder="Search podcasts..." value="{{ request.form.get('query', '') }}">
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
|
@ -46,15 +83,199 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© 2023 Podcastrr</p>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Task status polling
|
||||
let taskPollingInterval;
|
||||
let activeTasks = {};
|
||||
let needsRefresh = false;
|
||||
let seenTasks = JSON.parse(localStorage.getItem('seenTasks') || '{}');
|
||||
|
||||
// Elements
|
||||
const taskStatusArea = document.getElementById('task-status-area');
|
||||
const tasksContainer = document.getElementById('tasks-container');
|
||||
const closeTasksBtn = document.getElementById('close-tasks-btn');
|
||||
|
||||
// Close tasks panel
|
||||
closeTasksBtn.addEventListener('click', function() {
|
||||
taskStatusArea.style.display = 'none';
|
||||
|
||||
// Stop polling when the panel is closed
|
||||
clearInterval(taskPollingInterval);
|
||||
|
||||
// Clean up tasks on the server
|
||||
fetch('/api/tasks/clean', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ max_age_seconds: 0 }) // Clean all completed/failed tasks
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up old seen tasks (older than 1 day)
|
||||
cleanupSeenTasks();
|
||||
|
||||
// Start polling for tasks
|
||||
startTaskPolling();
|
||||
|
||||
function startTaskPolling() {
|
||||
// Initial fetch
|
||||
fetchTasks();
|
||||
|
||||
// Set up polling interval (every 2 seconds)
|
||||
taskPollingInterval = setInterval(fetchTasks, 2000);
|
||||
}
|
||||
|
||||
function fetchTasks() {
|
||||
fetch('/api/tasks')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
updateTasksUI(data.tasks);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching tasks:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTasksUI(tasks) {
|
||||
// Filter for active tasks (pending or running)
|
||||
const runningTasks = tasks.filter(task =>
|
||||
task.status === 'pending' || task.status === 'running'
|
||||
);
|
||||
|
||||
// Show task area if there are running tasks
|
||||
if (runningTasks.length > 0) {
|
||||
taskStatusArea.style.display = 'block';
|
||||
needsRefresh = true;
|
||||
}
|
||||
|
||||
// Update active tasks tracking
|
||||
activeTasks = {};
|
||||
runningTasks.forEach(task => {
|
||||
activeTasks[task.id] = true;
|
||||
});
|
||||
|
||||
// Clear container
|
||||
tasksContainer.innerHTML = '';
|
||||
|
||||
// Add task items
|
||||
runningTasks.forEach(task => {
|
||||
const taskElement = createTaskElement(task);
|
||||
tasksContainer.appendChild(taskElement);
|
||||
});
|
||||
|
||||
// If no running tasks but we have completed/failed tasks from this session, show them
|
||||
if (runningTasks.length === 0) {
|
||||
// Filter out tasks that have already been seen
|
||||
const recentTasks = tasks.filter(task =>
|
||||
(task.status === 'completed' || task.status === 'failed') &&
|
||||
new Date(task.completed_at) > new Date(Date.now() - 10000) && // Last 10 seconds
|
||||
!seenTasks[task.id] // Only show tasks that haven't been seen
|
||||
);
|
||||
|
||||
if (recentTasks.length > 0) {
|
||||
taskStatusArea.style.display = 'block';
|
||||
|
||||
// Mark these tasks as seen with current timestamp
|
||||
recentTasks.forEach(task => {
|
||||
seenTasks[task.id] = new Date().getTime();
|
||||
const taskElement = createTaskElement(task);
|
||||
tasksContainer.appendChild(taskElement);
|
||||
});
|
||||
|
||||
// Save seen tasks to localStorage
|
||||
localStorage.setItem('seenTasks', JSON.stringify(seenTasks));
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
taskStatusArea.style.display = 'none';
|
||||
}, 5000);
|
||||
|
||||
// If we had running tasks before and now they're complete, refresh the content
|
||||
// but only if we're on a page that would be affected by the task completion
|
||||
if (needsRefresh && window.location.pathname.includes('/podcasts/')) {
|
||||
// Add a small refresh button instead of auto-refreshing
|
||||
const refreshBtn = document.createElement('button');
|
||||
refreshBtn.className = 'btn btn-sm btn-primary';
|
||||
refreshBtn.style.marginTop = '8px';
|
||||
refreshBtn.style.width = '100%';
|
||||
refreshBtn.textContent = 'Refresh Content';
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
// Only reload the content, not the entire page
|
||||
const contentArea = document.querySelector('.content-area');
|
||||
if (contentArea) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
tasksContainer.appendChild(refreshBtn);
|
||||
needsRefresh = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupSeenTasks() {
|
||||
// Get the current seenTasks from localStorage
|
||||
const storedTasks = JSON.parse(localStorage.getItem('seenTasks') || '{}');
|
||||
const now = new Date().getTime();
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000; // 1 day in milliseconds
|
||||
const cleanedTasks = {};
|
||||
|
||||
// Only keep tasks that have timestamps and are less than 1 day old
|
||||
for (const taskId in storedTasks) {
|
||||
const timestamp = storedTasks[taskId];
|
||||
if (typeof timestamp === 'number' && now - timestamp < oneDayInMs) {
|
||||
cleanedTasks[taskId] = timestamp;
|
||||
} else if (storedTasks[taskId] === true) {
|
||||
// For tasks without timestamps (from previous version), add a timestamp now
|
||||
cleanedTasks[taskId] = now;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the cleaned tasks back to localStorage
|
||||
localStorage.setItem('seenTasks', JSON.stringify(cleanedTasks));
|
||||
seenTasks = cleanedTasks;
|
||||
}
|
||||
|
||||
function createTaskElement(task) {
|
||||
const taskDiv = document.createElement('div');
|
||||
taskDiv.className = 'task-item';
|
||||
taskDiv.style.padding = '8px';
|
||||
taskDiv.style.marginBottom = '8px';
|
||||
taskDiv.style.backgroundColor = '#21262d';
|
||||
taskDiv.style.borderRadius = '4px';
|
||||
taskDiv.style.fontSize = '11px';
|
||||
|
||||
// Status color
|
||||
let statusColor = '#7d8590'; // Default gray
|
||||
if (task.status === 'running') statusColor = '#3fb950'; // Green
|
||||
if (task.status === 'completed') statusColor = '#3fb950'; // Green
|
||||
if (task.status === 'failed') statusColor = '#f85149'; // Red
|
||||
|
||||
// Create task content
|
||||
const taskContent = `
|
||||
<div style="display: flex; align-items: center; margin-bottom: 4px;">
|
||||
<span style="font-weight: bold; color: #f0f6fc;">${task.description}</span>
|
||||
<span style="margin-left: auto; color: ${statusColor}; font-size: 10px;">${task.status}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 6px; color: #7d8590; font-size: 10px;">${task.message || ''}</div>
|
||||
<div class="progress-bar" style="height: 4px; background-color: #30363d; border-radius: 2px; overflow: hidden;">
|
||||
<div class="progress-fill" style="height: 100%; width: ${task.progress}%; background-color: ${statusColor};"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
taskDiv.innerHTML = taskContent;
|
||||
return taskDiv;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
@ -3,9 +3,20 @@
|
|||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="dashboard">
|
||||
<h2>Dashboard</h2>
|
||||
<div class="content-header">
|
||||
<h1 class="content-title">Dashboard</h1>
|
||||
<div class="content-actions">
|
||||
<button class="btn btn-sm" id="refresh-stats">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn" id="update-all">Update All</button>
|
||||
<button class="toolbar-btn" id="rss-sync">RSS Sync</button>
|
||||
<button class="toolbar-btn" id="clean-downloads">Clean Downloads</button>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Total Podcasts</h3>
|
||||
|
@ -13,41 +24,72 @@
|
|||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Recent Episodes</h3>
|
||||
<p class="stat-value">Coming Soon</p>
|
||||
<h3>Episodes</h3>
|
||||
<p class="stat-value">0</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Downloads</h3>
|
||||
<p class="stat-value">Coming Soon</p>
|
||||
<p class="stat-value">0</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Storage Used</h3>
|
||||
<p class="stat-value">Coming Soon</p>
|
||||
<h3>Storage</h3>
|
||||
<p class="stat-value">0 GB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="button-group">
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn">Search for Podcasts</a>
|
||||
<button class="btn" id="update-all">Update All Podcasts</button>
|
||||
<button class="btn" id="clean-downloads">Clean Old Downloads</button>
|
||||
<div style="padding: 1rem;">
|
||||
<h3 style="color: #cbd5e0; margin-bottom: 1rem;">Recent Activity</h3>
|
||||
<div class="empty-state" style="padding: 2rem;">
|
||||
<p>No recent activity to display.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Simple JavaScript for the buttons (to be implemented)
|
||||
document.getElementById('refresh-stats').addEventListener('click', function() {
|
||||
this.textContent = 'Refreshing...';
|
||||
this.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.textContent = 'Refresh';
|
||||
this.disabled = false;
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
document.getElementById('update-all').addEventListener('click', function() {
|
||||
alert('Update all podcasts functionality coming soon!');
|
||||
this.style.backgroundColor = '#5d9cec';
|
||||
this.textContent = 'Updating...';
|
||||
|
||||
setTimeout(() => {
|
||||
this.style.backgroundColor = '';
|
||||
this.textContent = 'Update All';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
document.getElementById('rss-sync').addEventListener('click', function() {
|
||||
this.style.backgroundColor = '#5d9cec';
|
||||
this.textContent = 'Syncing...';
|
||||
|
||||
setTimeout(() => {
|
||||
this.style.backgroundColor = '';
|
||||
this.textContent = 'RSS Sync';
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
document.getElementById('clean-downloads').addEventListener('click', function() {
|
||||
alert('Clean old downloads functionality coming soon!');
|
||||
if (confirm('Clean old downloads?')) {
|
||||
this.style.backgroundColor = '#e74c3c';
|
||||
this.textContent = 'Cleaning...';
|
||||
|
||||
setTimeout(() => {
|
||||
this.style.backgroundColor = '';
|
||||
this.textContent = 'Clean Downloads';
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
138
templates/debug/logs.html
Normal file
138
templates/debug/logs.html
Normal file
|
@ -0,0 +1,138 @@
|
|||
{ % extends
|
||||
"base.html" %}
|
||||
|
||||
{ % block
|
||||
title %}Debug
|
||||
Logs
|
||||
{ % endblock %}
|
||||
|
||||
{ % block
|
||||
content %}
|
||||
< div
|
||||
|
||||
|
||||
class ="page-header" >
|
||||
|
||||
< h1
|
||||
|
||||
|
||||
class ="page-title" > Debug Information < / h1 >
|
||||
|
||||
< / div >
|
||||
|
||||
< div
|
||||
|
||||
|
||||
class ="content-area" >
|
||||
|
||||
< div
|
||||
|
||||
|
||||
class ="debug-section" >
|
||||
|
||||
< h3 > Test
|
||||
RSS
|
||||
Feed < / h3 >
|
||||
< form
|
||||
id = "test-feed-form" >
|
||||
< input
|
||||
type = "url"
|
||||
id = "feed-url"
|
||||
placeholder = "Enter RSS feed URL"
|
||||
style = "width: 400px; padding: 8px;" >
|
||||
< button
|
||||
type = "submit"
|
||||
|
||||
|
||||
class ="btn btn-primary" > Test Feed < / button >
|
||||
|
||||
< / form >
|
||||
< div
|
||||
id = "feed-results"
|
||||
style = "margin-top: 20px;" > < / div >
|
||||
< / div >
|
||||
|
||||
< div
|
||||
|
||||
|
||||
class ="debug-section" style="margin-top: 30px;" >
|
||||
|
||||
< h3 > Recent
|
||||
Activity < / h3 >
|
||||
< div
|
||||
id = "recent-activity" >
|
||||
< p > Check
|
||||
browser
|
||||
console
|
||||
for detailed logs during podcast operations.< / p >
|
||||
< / div >
|
||||
< / div >
|
||||
< / div >
|
||||
|
||||
< script >
|
||||
document.getElementById('test-feed-form').addEventListener('submit', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
const
|
||||
feedUrl = document.getElementById('feed-url').value;
|
||||
const
|
||||
resultsDiv = document.getElementById('feed-results');
|
||||
|
||||
if (!feedUrl)
|
||||
{
|
||||
resultsDiv.innerHTML = '<div style="color: #f56565;">Please enter a feed URL</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsDiv.innerHTML = '<div style="color: #63b3ed;">Testing feed...</div>';
|
||||
|
||||
fetch(` / debug / test - feed?url =${encodeURIComponent(feedUrl)}
|
||||
`)
|
||||
.then(response= > response.json())
|
||||
.then(data= > {
|
||||
if (data.success)
|
||||
{
|
||||
let
|
||||
html = `
|
||||
< div
|
||||
style = "color: #48bb78; margin-bottom: 10px;" >
|
||||
✓ Feed
|
||||
test
|
||||
successful! Found ${data.episodes_found}
|
||||
episodes
|
||||
< / div >
|
||||
< div
|
||||
style = "background: #2d3748; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 12px;" >
|
||||
`;
|
||||
|
||||
data.episodes.forEach(episode= > {
|
||||
html += `
|
||||
< div
|
||||
style = "margin-bottom: 10px; border-bottom: 1px solid #4a5568; padding-bottom: 10px;" >
|
||||
< div
|
||||
style = "color: #63b3ed; font-weight: bold;" >${episode.title} < / div >
|
||||
< div
|
||||
style = "color: #a0aec0; margin: 5px 0;" >${episode.description} < / div >
|
||||
< div
|
||||
style = "color: #68d391; font-size: 11px;" > Published: ${episode.pub_date} < / div >
|
||||
< div
|
||||
style = "color: #fbb6ce; font-size: 11px;" > Audio: ${episode.audio_url} < / div >
|
||||
< / div >
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
resultsDiv.innerHTML = html;
|
||||
} else {
|
||||
resultsDiv.innerHTML = ` < div
|
||||
style = "color: #f56565;" >✗ Error: ${data.error} < / div > `;
|
||||
}
|
||||
})
|
||||
.catch(error= > {
|
||||
resultsDiv.innerHTML = ` < div
|
||||
style = "color: #f56565;" >✗ Network
|
||||
error: ${error.message} < / div > `;
|
||||
});
|
||||
});
|
||||
< / script >
|
||||
{ % endblock %}
|
|
@ -3,30 +3,68 @@
|
|||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<h2>Welcome to Podcastrr</h2>
|
||||
<p>A podcast management application similar to Sonarr but for podcasts.</p>
|
||||
</section>
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Podcasts</h1>
|
||||
<div class="page-actions">
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn btn-primary">Add New</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="recent-podcasts">
|
||||
<h3>Recent Podcasts</h3>
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn primary">Update All</button>
|
||||
<button class="toolbar-btn">RSS Sync</button>
|
||||
<button class="toolbar-btn">Options</button>
|
||||
<button class="toolbar-btn">View</button>
|
||||
<button class="toolbar-btn">Sort</button>
|
||||
<button class="toolbar-btn">Filter</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content-area">
|
||||
{% if recent_podcasts %}
|
||||
<div class="podcast-grid">
|
||||
{% for podcast in recent_podcasts %}
|
||||
<div class="podcast-card">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}">
|
||||
{% else %}
|
||||
<div class="no-image">No Image</div>
|
||||
{% endif %}
|
||||
<h4>{{ podcast.title }}</h4>
|
||||
<p class="author">{{ podcast.author }}</p>
|
||||
<a href="{{ url_for('podcasts.view', podcast_id=podcast.id) }}" class="btn">View Episodes</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"></th>
|
||||
<th>Podcast Title</th>
|
||||
<th>Network</th>
|
||||
<th>Quality Profile</th>
|
||||
<th>Next Airing</th>
|
||||
<th>Previous Airing</th>
|
||||
<th>Original Language</th>
|
||||
<th>Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for podcast in recent_podcasts %}
|
||||
<tr>
|
||||
<td class="cell-center">
|
||||
<span class="status-badge status-active">●</span>
|
||||
</td>
|
||||
<td class="cell-title">
|
||||
<a href="{{ url_for('podcasts.view', podcast_id=podcast.id) }}">
|
||||
{{ podcast.title }}
|
||||
</a>
|
||||
<div class="cell-secondary">{{ podcast.author or 'Unknown' }}</div>
|
||||
</td>
|
||||
<td class="cell-secondary">{{ podcast.author or 'Unknown' }}</td>
|
||||
<td class="cell-secondary">Any</td>
|
||||
<td class="cell-secondary">-</td>
|
||||
<td class="cell-secondary">{{ podcast.last_updated.strftime('%Y-%m-%d') if podcast.last_updated else 'Never' }}</td>
|
||||
<td class="cell-secondary">English</td>
|
||||
<td class="cell-secondary">{{ podcast.last_updated.strftime('%Y-%m-%d') if podcast.last_updated else 'Unknown' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No podcasts found. <a href="{{ url_for('podcasts.search') }}">Search for podcasts</a> to get started.</p>
|
||||
<div class="empty-state">
|
||||
<h3>No podcasts found</h3>
|
||||
<p>Get started by adding your first podcast</p>
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn btn-primary">Add Podcast</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,39 +1,111 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Your Podcasts{% endblock %}
|
||||
{% block title %}Podcasts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="podcasts">
|
||||
<div class="section-header">
|
||||
<h2>Your Podcasts</h2>
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn">Search for Podcasts</a>
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Podcasts</h1>
|
||||
<div class="page-actions">
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn btn-primary">Add New</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn primary" onclick="updateAllPodcasts()">Update All</button>
|
||||
<button class="toolbar-btn" onclick="refreshPage()">Refresh</button>
|
||||
<div style="margin-left: auto;">
|
||||
<span class="toolbar-btn">{{ podcasts|length }} Podcasts</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="content-area">
|
||||
{% if podcasts %}
|
||||
<div class="podcast-grid">
|
||||
{% for podcast in podcasts %}
|
||||
<div class="podcast-card">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}">
|
||||
{% else %}
|
||||
<div class="no-image">No Image</div>
|
||||
{% endif %}
|
||||
<h3>{{ podcast.title }}</h3>
|
||||
<p class="author">{{ podcast.author }}</p>
|
||||
<div class="podcast-actions">
|
||||
<a href="{{ url_for('podcasts.view', podcast_id=podcast.id) }}" class="btn">View Episodes</a>
|
||||
<form action="{{ url_for('podcasts.delete', podcast_id=podcast.id) }}" method="post" onsubmit="return confirm('Are you sure you want to delete this podcast?');">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;"></th>
|
||||
<th>Title</th>
|
||||
<th>Author</th>
|
||||
<th style="width: 100px;">Episodes</th>
|
||||
<th style="width: 120px;">Last Updated</th>
|
||||
<th style="width: 80px;">Status</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for podcast in podcasts %}
|
||||
<tr>
|
||||
<td class="cell-center">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}"
|
||||
style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;">
|
||||
{% else %}
|
||||
<div style="width: 40px; height: 40px; background-color: #30363d; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; color: #7d8590;">
|
||||
No Image
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-title">
|
||||
<a href="{{ url_for('podcasts.view', podcast_id=podcast.id) }}">{{ podcast.title }}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="cell-secondary">{{ podcast.author or 'Unknown' }}</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<span class="status-badge status-active">{{ podcast.episodes.count() }}</span>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="cell-secondary">
|
||||
{% if podcast.last_updated %}
|
||||
{{ podcast.last_updated.strftime('%Y-%m-%d') }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
{% if podcast.episodes.count() > 0 %}
|
||||
<span class="status-badge status-active">Active</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">Pending</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="cell-actions">
|
||||
<form action="{{ url_for('podcasts.update', podcast_id=podcast.id) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="btn btn-sm">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form action="{{ url_for('podcasts.delete', podcast_id=podcast.id) }}" method="post"
|
||||
style="display: inline; margin-left: 4px;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this podcast?');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h3>No Podcasts Found</h3>
|
||||
<p>You haven't added any podcasts yet.</p>
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn">Search for Podcasts</a>
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn btn-primary">Add Your First Podcast</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateAllPodcasts() {
|
||||
// This would trigger an update for all podcasts
|
||||
alert('Update All functionality would be implemented here');
|
||||
}
|
||||
|
||||
function refreshPage() {
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
81
templates/podcasts/naming_format_modal.html
Normal file
81
templates/podcasts/naming_format_modal.html
Normal file
|
@ -0,0 +1,81 @@
|
|||
<!-- Naming Format Modal -->
|
||||
<div id="naming-format-modal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7);">
|
||||
<div style="background-color: #161b22; margin: 10% auto; padding: 20px; border: 1px solid #30363d; border-radius: 6px; width: 80%; max-width: 600px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h2 style="margin: 0; font-size: 18px; color: #f0f6fc;">Configure Naming Format</h2>
|
||||
<span onclick="document.getElementById('naming-format-modal').style.display='none'" style="cursor: pointer; font-size: 20px; color: #7d8590;">×</span>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('podcasts.update_naming_format', podcast_id=podcast.id) }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="naming-format">Naming Format:</label>
|
||||
<select id="naming-format" name="naming_format" class="form-control" style="width: 100%; padding: 8px; margin-bottom: 16px; background-color: #21262d; border: 1px solid #30363d; color: #c9d1d9; border-radius: 4px;" onchange="updateCustomFormatVisibility()">
|
||||
<option value="">Use Global Settings</option>
|
||||
<option value="{podcast_title}/{episode_title}">Podcast Title / Episode Title</option>
|
||||
<option value="{podcast_title}/{episode_number} - {episode_title}">Podcast Title / Episode Number - Episode Title</option>
|
||||
<option value="{podcast_title}/S{season}E{episode_number} - {episode_title}">Podcast Title / Season Episode - Episode Title</option>
|
||||
<option value="{podcast_title}/Season {season}/{episode_number} - {episode_title}">Podcast Title / Season X / Episode Number - Episode Title</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="custom-format-group" class="form-group" style="display: none;">
|
||||
<label for="custom-format">Custom Format:</label>
|
||||
<input type="text" id="custom-format" name="custom_format" class="form-control" value="{{ podcast.naming_format }}" style="width: 100%; padding: 8px; margin-bottom: 8px; background-color: #21262d; border: 1px solid #30363d; color: #c9d1d9; border-radius: 4px;">
|
||||
<div style="font-size: 11px; color: #7d8590; margin-bottom: 16px;">
|
||||
Available variables: {podcast_title}, {episode_title}, {episode_number}, {season}, {season_episode}, {published_date}, {author}, {explicit}, {absolute_number}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<button type="button" onclick="document.getElementById('naming-format-modal').style.display='none'" class="btn" style="margin-right: 8px;">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Set the initial selected option based on the podcast's naming format
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const namingFormatSelect = document.getElementById('naming-format');
|
||||
const customFormatGroup = document.getElementById('custom-format-group');
|
||||
const customFormatInput = document.getElementById('custom-format');
|
||||
const podcastNamingFormat = "{{ podcast.naming_format }}";
|
||||
|
||||
// Set the selected option
|
||||
if (podcastNamingFormat) {
|
||||
let found = false;
|
||||
for (let i = 0; i < namingFormatSelect.options.length; i++) {
|
||||
if (namingFormatSelect.options[i].value === podcastNamingFormat) {
|
||||
namingFormatSelect.selectedIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// If the format doesn't match any predefined options, select "Custom"
|
||||
for (let i = 0; i < namingFormatSelect.options.length; i++) {
|
||||
if (namingFormatSelect.options[i].value === "custom") {
|
||||
namingFormatSelect.selectedIndex = i;
|
||||
customFormatGroup.style.display = "block";
|
||||
customFormatInput.value = podcastNamingFormat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateCustomFormatVisibility() {
|
||||
const namingFormatSelect = document.getElementById('naming-format');
|
||||
const customFormatGroup = document.getElementById('custom-format-group');
|
||||
|
||||
if (namingFormatSelect.value === "custom") {
|
||||
customFormatGroup.style.display = "block";
|
||||
} else {
|
||||
customFormatGroup.style.display = "none";
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,43 +1,65 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search for Podcasts{% endblock %}
|
||||
{% block title %}Add New Podcast{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="search">
|
||||
<h2>Search for Podcasts</h2>
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Add New Podcast</h1>
|
||||
</div>
|
||||
|
||||
<form method="post" class="search-form">
|
||||
<!-- Search Form -->
|
||||
<div class="form-container">
|
||||
<form action="{{ url_for('podcasts.search') }}" method="post">
|
||||
<div class="form-group">
|
||||
<input type="text" name="query" id="query" placeholder="Enter podcast name or keywords" required>
|
||||
<button type="submit" class="btn">Search</button>
|
||||
<label for="query">Search for podcasts:</label>
|
||||
<input type="text" id="query" name="query" value="{{ request.form.get('query', '') }}"
|
||||
placeholder="Enter podcast name, author, or keywords..." required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
{% if results %}
|
||||
<div class="content-area">
|
||||
<div style="padding: 16px; border-bottom: 1px solid #30363d; background-color: #161b22;">
|
||||
<h3 style="color: #f0f6fc; margin: 0; font-size: 14px;">Search Results ({{ results|length }})</h3>
|
||||
</div>
|
||||
|
||||
{% if results %}
|
||||
<div class="search-results">
|
||||
<h3>Search Results</h3>
|
||||
<div class="podcast-grid">
|
||||
{% for podcast in results %}
|
||||
<div class="podcast-card">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}">
|
||||
{% for result in results %}
|
||||
<div class="search-result-item">
|
||||
<div class="search-result-image">
|
||||
{% if result.image_url %}
|
||||
<img src="{{ result.image_url }}" alt="{{ result.title }}">
|
||||
{% else %}
|
||||
<div class="no-image">No Image</div>
|
||||
No Image
|
||||
{% endif %}
|
||||
<h4>{{ podcast.title }}</h4>
|
||||
<p class="author">{{ podcast.author }}</p>
|
||||
<p class="genre">{{ podcast.genre }}</p>
|
||||
<form action="{{ url_for('podcasts.add', podcast_id=podcast.external_id) }}" method="post">
|
||||
<button type="submit" class="btn">Add to Library</button>
|
||||
</div>
|
||||
<div class="search-result-info">
|
||||
<div class="search-result-title">{{ result.title }}</div>
|
||||
<div class="search-result-author">{{ result.author or 'Unknown Author' }}</div>
|
||||
{% if result.description %}
|
||||
<div class="search-result-description">{{ result.description }}</div>
|
||||
{% endif %}
|
||||
{% if result.genre %}
|
||||
<div style="font-size: 10px; color: #7d8590; margin-top: 4px;">Genre: {{ result.genre }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="search-result-actions">
|
||||
<form action="{{ url_for('podcasts.add', podcast_id=result.external_id) }}" method="post">
|
||||
<button type="submit" class="btn btn-primary">Add Podcast</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif request.method == 'POST' %}
|
||||
<div class="no-results">
|
||||
<p>No podcasts found matching your search. Try different keywords.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
{% elif request.method == 'POST' %}
|
||||
<div class="empty-state">
|
||||
<h3>No Results Found</h3>
|
||||
<p>No podcasts found for your search query. Try different keywords.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,84 +3,284 @@
|
|||
{% block title %}{{ podcast.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="podcast-detail">
|
||||
<div class="podcast-header">
|
||||
<div class="podcast-image">
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ podcast.title }}</h1>
|
||||
<div class="page-actions">
|
||||
<form action="{{ url_for('podcasts.update', podcast_id=podcast.id) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="btn btn-primary">Update Episodes</button>
|
||||
</form>
|
||||
<form action="{{ url_for('podcasts.delete', podcast_id=podcast.id) }}" method="post"
|
||||
style="display: inline; margin-left: 8px;"
|
||||
onsubmit="return confirm('Are you sure you want to delete this podcast?');">
|
||||
<button type="submit" class="btn btn-danger">Delete Podcast</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Info -->
|
||||
<div style="padding: 16px; background-color: #161b22; border-bottom: 1px solid #30363d;">
|
||||
<div style="display: flex; gap: 16px;">
|
||||
<div style="flex-shrink: 0;">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}">
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}"
|
||||
style="width: 120px; height: 120px; object-fit: cover; border-radius: 6px;">
|
||||
{% else %}
|
||||
<div class="no-image">No Image</div>
|
||||
<div style="width: 120px; height: 120px; background-color: #21262d; border-radius: 6px; display: flex; align-items: center; justify-content: center; color: #7d8590;">
|
||||
No Image
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="podcast-info">
|
||||
<h1>{{ podcast.title }}</h1>
|
||||
<p class="podcast-author">{{ podcast.author }}</p>
|
||||
<div class="podcast-description">
|
||||
<p>{{ podcast.description }}</p>
|
||||
<div style="flex: 1;">
|
||||
<h2 style="color: #f0f6fc; margin-bottom: 8px; font-size: 18px;">{{ podcast.title }}</h2>
|
||||
<p style="color: #7d8590; margin-bottom: 8px; font-size: 13px;">{{ podcast.author or 'Unknown Author' }}</p>
|
||||
{% if podcast.description %}
|
||||
<p style="color: #c9d1d9; font-size: 12px; line-height: 1.4; margin-bottom: 12px;">{{ podcast.description[:200] }}{% if podcast.description|length > 200 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
<div style="display: flex; gap: 16px; font-size: 11px; color: #7d8590;">
|
||||
<span>Episodes: {{ episodes|length }}</span>
|
||||
<span>Last Updated: {{ podcast.last_updated.strftime('%Y-%m-%d %H:%M') if podcast.last_updated else 'Never' }}</span>
|
||||
<span>Last Checked: {{ podcast.last_checked.strftime('%Y-%m-%d %H:%M') if podcast.last_checked else 'Never' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="podcast-actions">
|
||||
<form action="{{ url_for('podcasts.delete', podcast_id=podcast.id) }}" method="post" onsubmit="return confirm('Are you sure you want to delete this podcast?');">
|
||||
<button type="submit" class="btn btn-danger">Delete Podcast</button>
|
||||
</form>
|
||||
|
||||
<form action="{{ url_for('podcasts.update', podcast_id=podcast.id) }}" method="post">
|
||||
<button type="submit" class="btn">Update Episodes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="podcast-meta">
|
||||
<span>Last updated: {{ podcast.last_updated.strftime('%Y-%m-%d %H:%M') if podcast.last_updated else 'Never' }}</span>
|
||||
<span>Last checked: {{ podcast.last_checked.strftime('%Y-%m-%d %H:%M') if podcast.last_checked else 'Never' }}</span>
|
||||
<div style="display: flex; gap: 16px; margin-top: 8px; font-size: 11px;">
|
||||
{% if podcast.feed_url %}
|
||||
<a href="{{ podcast.feed_url }}" target="_blank" style="color: #58a6ff;">View RSS Feed</a>
|
||||
{% endif %}
|
||||
<a href="#" onclick="document.getElementById('naming-format-modal').style.display='block'; return false;" style="color: #58a6ff;">Configure Naming Format</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="episodes-section">
|
||||
<div class="episodes-header">
|
||||
<h2>Episodes ({{ episodes|length }})</h2>
|
||||
</div>
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<span class="toolbar-btn">{{ episodes|length }} Episodes</span>
|
||||
<div style="margin-left: auto;">
|
||||
<form action="{{ url_for('podcasts.verify', podcast_id=podcast.id) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="toolbar-btn">Verify Files</button>
|
||||
</form>
|
||||
<button class="toolbar-btn" onclick="window.location.reload()">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if episodes %}
|
||||
<div class="episodes-list">
|
||||
|
||||
<!-- Episodes Table -->
|
||||
<div class="content-area">
|
||||
{% if episodes %}
|
||||
{# Check if any episodes have season information #}
|
||||
{% set has_seasons = false %}
|
||||
{% for episode in episodes %}
|
||||
{% if episode.season and not has_seasons %}
|
||||
{% set has_seasons = true %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if has_seasons %}
|
||||
{# Group episodes by season #}
|
||||
{% set seasons = {} %}
|
||||
{% for episode in episodes %}
|
||||
<div class="episode-item">
|
||||
<div class="episode-header">
|
||||
<h3 class="episode-title">{{ episode.title }}</h3>
|
||||
<span class="episode-date">{{ episode.published_date.strftime('%Y-%m-%d') if episode.published_date else 'Unknown date' }}</span>
|
||||
</div>
|
||||
<div class="episode-description">
|
||||
<p>{{ episode.description|truncate(200) }}</p>
|
||||
</div>
|
||||
<div class="episode-footer">
|
||||
<div class="episode-meta">
|
||||
{% if episode.duration %}
|
||||
<span class="episode-duration">Duration: {{ (episode.duration / 60)|int }} min</span>
|
||||
{% endif %}
|
||||
{% set season_num = episode.season|default(0) %}
|
||||
{% if season_num not in seasons %}
|
||||
{% set seasons = seasons|merge({season_num: []}) %}
|
||||
{% endif %}
|
||||
{% set _ = seasons[season_num].append(episode) %}
|
||||
{% endfor %}
|
||||
|
||||
{% if episode.episode_number %}
|
||||
<span class="episode-number">Episode: {{ episode.episode_number }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="episode-actions">
|
||||
{% if episode.downloaded %}
|
||||
<span class="badge downloaded">Downloaded</span>
|
||||
{# Display seasons in order #}
|
||||
{% for season_num in seasons|sort %}
|
||||
<div class="season-accordion">
|
||||
<div class="season-header" onclick="toggleSeason('{{ season_num }}')">
|
||||
<h3>
|
||||
{% if season_num == 0 %}
|
||||
Unsorted Episodes
|
||||
{% else %}
|
||||
<a href="{{ url_for('podcasts.download', episode_id=episode.id) }}" class="btn btn-sm">Download</a>
|
||||
Season {{ season_num }}
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ episode.audio_url }}" target="_blank" class="btn btn-sm btn-secondary">Stream</a>
|
||||
</div>
|
||||
<span class="episode-count">({{ seasons[season_num]|length }} episodes)</span>
|
||||
</h3>
|
||||
<span id="toggle-icon-{{ season_num }}" class="toggle-icon">▼</span>
|
||||
</div>
|
||||
<div id="season-{{ season_num }}" class="season-content">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Episode</th>
|
||||
<th style="width: 100px;">Published</th>
|
||||
<th style="width: 80px;">Duration</th>
|
||||
<th style="width: 80px;">Status</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for episode in seasons[season_num]|sort(attribute='episode_number') %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cell-title">
|
||||
{% if episode.episode_number %}
|
||||
<span style="color: #58a6ff; font-weight: bold; margin-right: 5px;">
|
||||
{% if episode.season %}
|
||||
S{{ episode.season }}E{{ episode.episode_number }}
|
||||
{% else %}
|
||||
#{{ episode.episode_number }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{{ episode.title }}
|
||||
{% if episode.explicit %}
|
||||
<span class="explicit-badge" title="Explicit Content">E</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if episode.description %}
|
||||
<div class="cell-secondary" style="margin-top: 4px;">
|
||||
{{ episode.description[:100] }}{% if episode.description|length > 100 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="cell-secondary">
|
||||
{{ episode.published_date.strftime('%Y-%m-%d') if episode.published_date else 'Unknown' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="cell-secondary">
|
||||
{% if episode.duration %}
|
||||
{{ (episode.duration / 60)|int }}m
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
{% if episode.downloaded %}
|
||||
<span class="status-badge status-active">Downloaded</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">Available</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="cell-actions">
|
||||
{% if not episode.downloaded %}
|
||||
<a href="{{ url_for('podcasts.download', episode_id=episode.id) }}" class="btn btn-sm">Download</a>
|
||||
{% else %}
|
||||
<form action="{{ url_for('podcasts.rename_episode', episode_id=episode.id) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="btn btn-sm">Rename</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if episode.audio_url %}
|
||||
<a href="{{ episode.audio_url }}" target="_blank" class="btn btn-sm btn-secondary" style="margin-left: 4px;">Stream</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-episodes">
|
||||
<p>No episodes found for this podcast. Try clicking the "Update Episodes" button to fetch the latest episodes.</p>
|
||||
</div>
|
||||
{# Display episodes in a flat table if no season information is available #}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Episode</th>
|
||||
<th style="width: 100px;">Published</th>
|
||||
<th style="width: 80px;">Duration</th>
|
||||
<th style="width: 80px;">Status</th>
|
||||
<th style="width: 120px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for episode in episodes %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="cell-title">
|
||||
{% if episode.episode_number %}
|
||||
<span style="color: #58a6ff; font-weight: bold; margin-right: 5px;">#{{ episode.episode_number }}</span>
|
||||
{% endif %}
|
||||
{{ episode.title }}
|
||||
{% if episode.explicit %}
|
||||
<span class="explicit-badge" title="Explicit Content">E</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if episode.description %}
|
||||
<div class="cell-secondary" style="margin-top: 4px;">
|
||||
{{ episode.description[:100] }}{% if episode.description|length > 100 %}...{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="cell-secondary">
|
||||
{{ episode.published_date.strftime('%Y-%m-%d') if episode.published_date else 'Unknown' }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="cell-secondary">
|
||||
{% if episode.duration %}
|
||||
{{ (episode.duration / 60)|int }}m
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
{% if episode.downloaded %}
|
||||
<span class="status-badge status-active">Downloaded</span>
|
||||
{% else %}
|
||||
<span class="status-badge status-pending">Available</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="cell-actions">
|
||||
{% if not episode.downloaded %}
|
||||
<a href="{{ url_for('podcasts.download', episode_id=episode.id) }}" class="btn btn-sm">Download</a>
|
||||
{% else %}
|
||||
<form action="{{ url_for('podcasts.rename_episode', episode_id=episode.id) }}" method="post" style="display: inline;">
|
||||
<button type="submit" class="btn btn-sm">Rename</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if episode.audio_url %}
|
||||
<a href="{{ episode.audio_url }}" target="_blank" class="btn btn-sm btn-secondary" style="margin-left: 4px;">Stream</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h3>No Episodes Found</h3>
|
||||
<p>No episodes found for this podcast.</p>
|
||||
<form action="{{ url_for('podcasts.update', podcast_id=podcast.id) }}" method="post">
|
||||
<button type="submit" class="btn btn-primary">Try Updating Episodes</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function toggleSeason(seasonId) {
|
||||
const seasonContent = document.getElementById('season-' + seasonId);
|
||||
const toggleIcon = document.getElementById('toggle-icon-' + seasonId);
|
||||
|
||||
if (seasonContent.style.display === 'block') {
|
||||
seasonContent.style.display = 'none';
|
||||
toggleIcon.innerHTML = '▼';
|
||||
} else {
|
||||
seasonContent.style.display = 'block';
|
||||
toggleIcon.innerHTML = '▲';
|
||||
}
|
||||
}
|
||||
|
||||
// Open the first season by default when the page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const firstSeasonAccordion = document.querySelector('.season-accordion');
|
||||
if (firstSeasonAccordion) {
|
||||
const seasonId = firstSeasonAccordion.querySelector('.season-content').id.replace('season-', '');
|
||||
toggleSeason(seasonId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include 'podcasts/naming_format_modal.html' %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue