Initial commit
This commit is contained in:
commit
e86ab53de5
35 changed files with 2638 additions and 0 deletions
25
templates/about.html
Normal file
25
templates/about.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}About{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="about">
|
||||
<h2>About Podcastrr</h2>
|
||||
<p>Podcastrr is a podcast management application similar to Sonarr but for podcasts, built with Python and Flask.</p>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>Search for Podcasts</strong>: Find podcasts from various sources</li>
|
||||
<li><strong>Track Podcasts</strong>: Monitor your favorite podcasts for new episodes</li>
|
||||
<li><strong>Download Management</strong>: Automatically download new episodes and manage storage</li>
|
||||
<li><strong>Custom Naming</strong>: Configure how downloaded files are named</li>
|
||||
<li><strong>Web Interface</strong>: Manage everything through an intuitive web interface</li>
|
||||
</ul>
|
||||
|
||||
<h3>Version</h3>
|
||||
<p>1.0.0</p>
|
||||
|
||||
<h3>License</h3>
|
||||
<p>MIT</p>
|
||||
</section>
|
||||
{% endblock %}
|
60
templates/base.html
Normal file
60
templates/base.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ title }}{% endblock %} - Podcastrr</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-container">
|
||||
<!-- Header with app name and search -->
|
||||
<header class="main-header">
|
||||
<div class="logo">
|
||||
<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>
|
||||
</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">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-message {{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>© 2023 Podcastrr</p>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
53
templates/dashboard.html
Normal file
53
templates/dashboard.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="dashboard">
|
||||
<h2>Dashboard</h2>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Total Podcasts</h3>
|
||||
<p class="stat-value">{{ total_podcasts }}</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Recent Episodes</h3>
|
||||
<p class="stat-value">Coming Soon</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Downloads</h3>
|
||||
<p class="stat-value">Coming Soon</p>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<h3>Storage Used</h3>
|
||||
<p class="stat-value">Coming Soon</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>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Simple JavaScript for the buttons (to be implemented)
|
||||
document.getElementById('update-all').addEventListener('click', function() {
|
||||
alert('Update all podcasts functionality coming soon!');
|
||||
});
|
||||
|
||||
document.getElementById('clean-downloads').addEventListener('click', function() {
|
||||
alert('Clean old downloads functionality coming soon!');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
32
templates/index.html
Normal file
32
templates/index.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<section class="recent-podcasts">
|
||||
<h3>Recent Podcasts</h3>
|
||||
{% 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>
|
||||
{% else %}
|
||||
<p>No podcasts found. <a href="{{ url_for('podcasts.search') }}">Search for podcasts</a> to get started.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
39
templates/podcasts/index.html
Normal file
39
templates/podcasts/index.html
Normal file
|
@ -0,0 +1,39 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Your 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>
|
||||
</div>
|
||||
|
||||
{% 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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>You haven't added any podcasts yet.</p>
|
||||
<a href="{{ url_for('podcasts.search') }}" class="btn">Search for Podcasts</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
43
templates/podcasts/search.html
Normal file
43
templates/podcasts/search.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search for Podcasts{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="search">
|
||||
<h2>Search for Podcasts</h2>
|
||||
|
||||
<form method="post" class="search-form">
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% 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 }}">
|
||||
{% else %}
|
||||
<div class="no-image">No Image</div>
|
||||
{% 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>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif request.method == 'POST' %}
|
||||
<div class="no-results">
|
||||
<p>No podcasts found matching your search. Try different keywords.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
86
templates/podcasts/view.html
Normal file
86
templates/podcasts/view.html
Normal file
|
@ -0,0 +1,86 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ podcast.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="podcast-detail">
|
||||
<div class="podcast-header">
|
||||
<div class="podcast-image">
|
||||
{% if podcast.image_url %}
|
||||
<img src="{{ podcast.image_url }}" alt="{{ podcast.title }}">
|
||||
{% else %}
|
||||
<div class="no-image">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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="episodes-section">
|
||||
<div class="episodes-header">
|
||||
<h2>Episodes ({{ episodes|length }})</h2>
|
||||
</div>
|
||||
|
||||
{% if episodes %}
|
||||
<div class="episodes-list">
|
||||
{% 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 %}
|
||||
|
||||
{% 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>
|
||||
{% else %}
|
||||
<a href="{{ url_for('podcasts.download', episode_id=episode.id) }}" class="btn btn-sm">Download</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ episode.audio_url }}" target="_blank" class="btn btn-sm btn-secondary">Stream</a>
|
||||
</div>
|
||||
</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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
73
templates/settings/index.html
Normal file
73
templates/settings/index.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="settings">
|
||||
<h2>Application Settings</h2>
|
||||
|
||||
<form method="post" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="download_path">Download Path</label>
|
||||
<input type="text" name="download_path" id="download_path" value="{{ settings.download_path }}" required>
|
||||
<small>Directory where podcast episodes will be downloaded</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="naming_format">Naming Format</label>
|
||||
<input type="text" name="naming_format" id="naming_format" value="{{ settings.naming_format }}" required>
|
||||
<small>Format for downloaded files. Available variables: {podcast_title}, {episode_title}, {published_date}, {episode_number}, {author}</small>
|
||||
<div id="naming-preview" class="preview">
|
||||
<strong>Preview:</strong> <span id="preview-text">{{ settings.naming_format }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox">
|
||||
<input type="checkbox" name="auto_download" id="auto_download" {% if settings.auto_download %}checked{% endif %}>
|
||||
<label for="auto_download">Auto-download new episodes</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="max_downloads">Maximum Concurrent Downloads</label>
|
||||
<input type="number" name="max_downloads" id="max_downloads" value="{{ settings.max_downloads }}" min="1" max="10" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="delete_after_days">Delete Episodes After (days)</label>
|
||||
<input type="number" name="delete_after_days" id="delete_after_days" value="{{ settings.delete_after_days }}" min="0" required>
|
||||
<small>Set to 0 to never delete</small>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Preview naming format
|
||||
const namingFormatInput = document.getElementById('naming_format');
|
||||
const previewText = document.getElementById('preview-text');
|
||||
|
||||
namingFormatInput.addEventListener('input', function() {
|
||||
// Send AJAX request to get preview
|
||||
fetch('{{ url_for("settings.naming_preview") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'naming_format=' + encodeURIComponent(this.value)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.preview) {
|
||||
previewText.textContent = data.preview;
|
||||
} else if (data.error) {
|
||||
previewText.textContent = 'Error: ' + data.error;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue