Add podgrab featureset
This commit is contained in:
parent
095bf52a2f
commit
233dd5b5c0
33 changed files with 2315 additions and 125 deletions
38
templates/podcasts/import_opml.html
Normal file
38
templates/podcasts/import_opml.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Import Podcasts from OPML</h1>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p>Upload an OPML file to import podcasts. OPML files are commonly used to export podcast subscriptions from other podcast applications.</p>
|
||||
|
||||
<form action="{{ url_for('podcasts.import_opml') }}" method="post" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="opml_file">OPML File</label>
|
||||
<input type="file" class="form-control-file" id="opml_file" name="opml_file" required>
|
||||
<small class="form-text text-muted">Select an OPML file (.opml or .xml)</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Import</button>
|
||||
<a href="{{ url_for('podcasts.index') }}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h2>What is OPML?</h2>
|
||||
<p>OPML (Outline Processor Markup Language) is a format commonly used to exchange lists of RSS feeds between applications. Most podcast applications allow you to export your subscriptions as an OPML file, which you can then import into Podcastrr.</p>
|
||||
|
||||
<h2>How to Export OPML from Other Applications</h2>
|
||||
<ul>
|
||||
<li><strong>Apple Podcasts:</strong> Go to File > Library > Export Library</li>
|
||||
<li><strong>Pocket Casts:</strong> Go to Profile > Settings > Export OPML</li>
|
||||
<li><strong>Spotify:</strong> Spotify doesn't support OPML export directly</li>
|
||||
<li><strong>Google Podcasts:</strong> Go to Settings > Export subscriptions</li>
|
||||
<li><strong>Overcast:</strong> Go to Settings > Export OPML</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -20,6 +20,29 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Add by RSS URL Form -->
|
||||
<div class="form-container mt-4">
|
||||
<h3>Add by RSS Feed URL</h3>
|
||||
<form action="{{ url_for('podcasts.add_by_url') }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="feed_url">Podcast RSS Feed URL:</label>
|
||||
<input type="url" id="feed_url" name="feed_url"
|
||||
placeholder="https://example.com/podcast/feed.xml" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add Podcast</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- OPML Import/Export -->
|
||||
<div class="form-container mt-4">
|
||||
<h3>Import/Export Podcasts</h3>
|
||||
<p>Import podcasts from an OPML file or export your current podcasts to an OPML file.</p>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('podcasts.import_opml') }}" class="btn btn-primary">Import OPML</a>
|
||||
<a href="{{ url_for('podcasts.export_opml') }}" class="btn btn-secondary">Export OPML</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
{% if results %}
|
||||
<div class="content-area">
|
||||
|
|
96
templates/podcasts/tags_modal.html
Normal file
96
templates/podcasts/tags_modal.html
Normal file
|
@ -0,0 +1,96 @@
|
|||
<!-- Tags Modal -->
|
||||
<div id="tags-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Manage Tags for {{ podcast.title }}</h2>
|
||||
<span class="close" onclick="document.getElementById('tags-modal').style.display='none'">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{{ url_for('podcasts.update_tags', podcast_id=podcast.id) }}" method="post">
|
||||
<div class="form-group">
|
||||
<label for="tags">Tags (comma-separated):</label>
|
||||
<input type="text" id="tags" name="tags" class="form-control"
|
||||
value="{{ podcast.tags }}"
|
||||
placeholder="news, technology, comedy, etc.">
|
||||
<small class="form-text text-muted">Enter tags separated by commas. Tags help you organize and filter your podcasts.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<h4>Common Tags</h4>
|
||||
<div class="tag-suggestions">
|
||||
<span class="tag-suggestion" onclick="addTag('news')">news</span>
|
||||
<span class="tag-suggestion" onclick="addTag('technology')">technology</span>
|
||||
<span class="tag-suggestion" onclick="addTag('comedy')">comedy</span>
|
||||
<span class="tag-suggestion" onclick="addTag('business')">business</span>
|
||||
<span class="tag-suggestion" onclick="addTag('politics')">politics</span>
|
||||
<span class="tag-suggestion" onclick="addTag('education')">education</span>
|
||||
<span class="tag-suggestion" onclick="addTag('entertainment')">entertainment</span>
|
||||
<span class="tag-suggestion" onclick="addTag('health')">health</span>
|
||||
<span class="tag-suggestion" onclick="addTag('science')">science</span>
|
||||
<span class="tag-suggestion" onclick="addTag('sports')">sports</span>
|
||||
<span class="tag-suggestion" onclick="addTag('arts')">arts</span>
|
||||
<span class="tag-suggestion" onclick="addTag('music')">music</span>
|
||||
<span class="tag-suggestion" onclick="addTag('favorites')">favorites</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">Save Tags</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('tags-modal').style.display='none'">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addTag(tag) {
|
||||
const tagsInput = document.getElementById('tags');
|
||||
const currentTags = tagsInput.value.split(',').map(t => t.trim()).filter(t => t);
|
||||
|
||||
// Add the tag if it's not already in the list
|
||||
if (!currentTags.includes(tag)) {
|
||||
currentTags.push(tag);
|
||||
tagsInput.value = currentTags.join(', ');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tag-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
margin-right: 4px;
|
||||
background-color: #1f6feb;
|
||||
color: #ffffff;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-badge:hover {
|
||||
background-color: #388bfd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tag-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tag-suggestion {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
background-color: #21262d;
|
||||
color: #c9d1d9;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-suggestion:hover {
|
||||
background-color: #30363d;
|
||||
}
|
||||
</style>
|
|
@ -10,6 +10,9 @@
|
|||
<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.download_all', podcast_id=podcast.id) }}" method="post" style="display: inline; margin-left: 8px;">
|
||||
<button type="submit" class="btn btn-success">Download All 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?');">
|
||||
|
@ -47,7 +50,17 @@
|
|||
<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>
|
||||
<a href="#" onclick="document.getElementById('tags-modal').style.display='block'; return false;" style="color: #58a6ff;">Manage Tags</a>
|
||||
</div>
|
||||
|
||||
{% if podcast.tags %}
|
||||
<div style="margin-top: 8px;">
|
||||
<span style="font-size: 11px; color: #7d8590;">Tags: </span>
|
||||
{% for tag in podcast.get_tags() %}
|
||||
<a href="{{ url_for('podcasts.filter_by_tag', tag=tag) }}" class="tag-badge">{{ tag }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -55,8 +68,8 @@
|
|||
<!-- 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;">
|
||||
<div style="margin-left: auto; display: flex; gap: 8px;">
|
||||
<form action="{{ url_for('podcasts.verify', podcast_id=podcast.id) }}" method="post" style="display: inline-block;">
|
||||
<button type="submit" class="toolbar-btn">Verify Files</button>
|
||||
</form>
|
||||
<button class="toolbar-btn" onclick="window.location.reload()">Refresh</button>
|
||||
|
@ -67,40 +80,62 @@
|
|||
<!-- Episodes Table -->
|
||||
<div class="content-area">
|
||||
{% if episodes %}
|
||||
{# Check if any episodes have season information #}
|
||||
{% set has_seasons = false %}
|
||||
{# Group episodes by season or year if season is not available #}
|
||||
{% set seasons = {} %}
|
||||
{% set season_ids = {} %}
|
||||
{% set season_download_counts = {} %}
|
||||
{% set season_counter = 0 %}
|
||||
|
||||
{% for episode in episodes %}
|
||||
{% if episode.season and not has_seasons %}
|
||||
{% set has_seasons = true %}
|
||||
{% set season_key = "" %}
|
||||
{% if episode.season %}
|
||||
{# Use season number if available #}
|
||||
{% set season_key = "Season " ~ episode.season %}
|
||||
{% elif episode.published_date %}
|
||||
{# Use year as season if no season number but published date is available #}
|
||||
{% set season_key = episode.published_date.strftime('%Y') %}
|
||||
{% else %}
|
||||
{# Fallback for episodes with no season or published date #}
|
||||
{% set season_key = "Unsorted Episodes" %}
|
||||
{% endif %}
|
||||
|
||||
{# Initialize season if not exists #}
|
||||
{% if season_key not in seasons %}
|
||||
{% set season_counter = season_counter + 1 %}
|
||||
{% set _ = seasons.update({season_key: []}) %}
|
||||
{% set _ = season_ids.update({season_key: season_counter}) %}
|
||||
{% set _ = season_download_counts.update({season_key: {'downloaded': 0, 'total': 0}}) %}
|
||||
{% endif %}
|
||||
|
||||
{# Add episode to season #}
|
||||
{% set _ = seasons[season_key].append(episode) %}
|
||||
|
||||
{# Update download counts #}
|
||||
{% if episode.downloaded %}
|
||||
{% set downloaded = season_download_counts[season_key]['downloaded'] + 1 %}
|
||||
{% set total = season_download_counts[season_key]['total'] + 1 %}
|
||||
{% else %}
|
||||
{% set downloaded = season_download_counts[season_key]['downloaded'] %}
|
||||
{% set total = season_download_counts[season_key]['total'] + 1 %}
|
||||
{% endif %}
|
||||
{% set _ = season_download_counts.update({season_key: {'downloaded': downloaded, 'total': total}}) %}
|
||||
{% endfor %}
|
||||
|
||||
{% if has_seasons %}
|
||||
{# Group episodes by season #}
|
||||
{% set seasons = {} %}
|
||||
{% for episode in episodes %}
|
||||
{% 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 %}
|
||||
{# Display seasons in reverse order (newest first) #}
|
||||
{% if seasons %}
|
||||
{% for season_key, episodes_list in seasons|dictsort|reverse %}
|
||||
{% set season_id = season_ids[season_key] %}
|
||||
{% set download_stats = season_download_counts[season_key] %}
|
||||
|
||||
{# Display seasons in order #}
|
||||
{% for season_num in seasons|sort %}
|
||||
<div class="season-accordion">
|
||||
<div class="season-header" onclick="toggleSeason('{{ season_num }}')">
|
||||
<div class="season-header" onclick="toggleSeason()">
|
||||
<h3>
|
||||
{% if season_num == 0 %}
|
||||
Unsorted Episodes
|
||||
{% else %}
|
||||
Season {{ season_num }}
|
||||
{% endif %}
|
||||
<span class="episode-count">({{ seasons[season_num]|length }} episodes)</span>
|
||||
{{ season_key }}
|
||||
<span class="episode-count">({{ download_stats['downloaded'] }}/{{ download_stats['total'] }} episodes)</span>
|
||||
</h3>
|
||||
<span id="toggle-icon-{{ season_num }}" class="toggle-icon">▼</span>
|
||||
<span id="toggle-icon-season_{{ season_id }}" class="toggle-icon">▼</span>
|
||||
</div>
|
||||
<div id="season-{{ season_num }}" class="season-content">
|
||||
<div id="season-season_{{ season_id }}" class="season-content">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -112,16 +147,16 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for episode in seasons[season_num]|sort(attribute='episode_number') %}
|
||||
{% for episode in episodes_list|sort(attribute='published_date', reverse=true) %}
|
||||
<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 }}
|
||||
S{{ '%02d' % episode.season }}E{{ '%02d' % episode.episode_number|int if episode.episode_number|string|isdigit() else episode.episode_number }}
|
||||
{% else %}
|
||||
#{{ episode.episode_number }}
|
||||
#{{ '%02d' % episode.episode_number|int if episode.episode_number|string|isdigit() else episode.episode_number }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
@ -194,7 +229,7 @@
|
|||
<td>
|
||||
<div class="cell-title">
|
||||
{% if episode.episode_number %}
|
||||
<span style="color: #58a6ff; font-weight: bold; margin-right: 5px;">#{{ episode.episode_number }}</span>
|
||||
<span style="color: #58a6ff; font-weight: bold; margin-right: 5px;">#{{ '%02d' % episode.episode_number|int if episode.episode_number|string|isdigit() else episode.episode_number }}</span>
|
||||
{% endif %}
|
||||
{{ episode.title }}
|
||||
{% if episode.explicit %}
|
||||
|
@ -260,27 +295,46 @@
|
|||
{% block scripts %}
|
||||
<script>
|
||||
function toggleSeason(seasonId) {
|
||||
const seasonContent = document.getElementById('season-' + seasonId);
|
||||
const toggleIcon = document.getElementById('toggle-icon-' + seasonId);
|
||||
// Find the clicked header element
|
||||
const clickedHeader = event.currentTarget;
|
||||
|
||||
// Find the content and toggle icon elements
|
||||
const seasonContent = clickedHeader.nextElementSibling;
|
||||
const toggleIcon = clickedHeader.querySelector('.toggle-icon');
|
||||
|
||||
if (seasonContent.style.display === 'block') {
|
||||
// If already open, close it
|
||||
seasonContent.style.display = 'none';
|
||||
toggleIcon.innerHTML = '▼';
|
||||
} else {
|
||||
// Close all other accordions first
|
||||
const allSeasonContents = document.querySelectorAll('.season-content');
|
||||
const allToggleIcons = document.querySelectorAll('.toggle-icon');
|
||||
|
||||
allSeasonContents.forEach(function(content) {
|
||||
content.style.display = 'none';
|
||||
});
|
||||
|
||||
allToggleIcons.forEach(function(icon) {
|
||||
icon.innerHTML = '▼';
|
||||
});
|
||||
|
||||
// Then open the clicked one
|
||||
seasonContent.style.display = 'block';
|
||||
toggleIcon.innerHTML = '▲';
|
||||
}
|
||||
}
|
||||
|
||||
// Open the first season by default when the page loads
|
||||
// Initialize all season accordions as collapsed by default
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const firstSeasonAccordion = document.querySelector('.season-accordion');
|
||||
if (firstSeasonAccordion) {
|
||||
const seasonId = firstSeasonAccordion.querySelector('.season-content').id.replace('season-', '');
|
||||
toggleSeason(seasonId);
|
||||
}
|
||||
// Make sure all season contents have display style set to none (collapsed)
|
||||
const allSeasonContents = document.querySelectorAll('.season-content');
|
||||
allSeasonContents.forEach(function(content) {
|
||||
content.style.display = 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% include 'podcasts/naming_format_modal.html' %}
|
||||
{% include 'podcasts/tags_modal.html' %}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue