This commit is contained in:
Cody Cook 2025-06-15 21:20:30 -07:00
commit 095bf52a2f
29 changed files with 2494 additions and 758 deletions

View file

@ -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 %}

View 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;">&times;</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>

View file

@ -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 %}

View file

@ -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 %}