Dateien nach "templates" hochladen

HTML
This commit is contained in:
Chaoscat 2025-08-08 10:56:22 +00:00
parent 7dd35b89fb
commit 278422f934
5 changed files with 755 additions and 0 deletions

84
templates/base.html Normal file
View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AniWorld Downloader{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">
<i class="bi bi-cloud-download"></i> The BOX
</a>
{% if current_user.is_authenticated %}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('index') }}">
<i class="bi bi-house"></i> Home
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('search') }}">
<i class="bi bi-search"></i> Suchen
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('queue') }}">
<i class="bi bi-list-check"></i> Warteschlange
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('settings') }}">
<i class="bi bi-gear"></i> Einstellungen
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">
<i class="bi bi-box-arrow-right"></i> Abmelden
</a>
</li>
</ul>
</div>
{% endif %}
</div>
</nav>
<main class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'message' else 'warning' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="container mt-5 mb-3 text-center">
<div class="row">
<div class="col-12">
<p class="text-muted small">
Made by CCN | Web Version 1.2
</p>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
{% block extra_scripts %}{% endblock %}
</body>
</html>

392
templates/index.html Normal file
View File

@ -0,0 +1,392 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-12 mb-4 text-center">
<h1 class="display-4">AniWorld Downloader</h1>
<p class="lead">Willkommen, {{ current_user.jellyfin_username }}!</p>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-link-45deg"></i> Link eingeben</h5>
</div>
<div class="card-body">
<div class="input-group mb-3">
<input type="url" class="form-control" id="animeLink"
placeholder="https://aniworld.to/anime/stream/...">
<button class="btn btn-primary" type="button" id="analyzeButton">
<i class="bi bi-search"></i> Analysieren
</button>
</div>
<div id="linkAnalysis" class="d-none">
<h5 class="mt-4 mb-3" id="contentTitle">Titel wird geladen...</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Typ</label>
<select class="form-select" id="contentType">
<option value="episodes">Episoden</option>
<option value="movies">Filme</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Provider</label>
<select class="form-select" id="provider">
<option value="Vidmoly">Vidmoly</option>
<option value="VOE">VOE</option>
<option value="SpeedFiles">SpeedFiles</option>
<option value="Vidoza">Vidoza</option>
<option value="Doodstream">Doodstream</option>
<option value="Streamtape">Streamtape</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Sprache (Prioritätsreihenfolge)</label>
<select class="form-select" id="language">
<option value="German">Deutsch</option>
<option value="Ger-Sub">Deutsch Sub</option>
<option value="Eng-Sub">English Sub</option>
<option value="English">English</option>
</select>
</div>
<div class="col-md-6" id="seasonContainer">
<label class="form-label">Staffel</label>
<div class="d-flex">
<select class="form-select me-2" id="seasonStart">
<option value="1">1</option>
</select>
<span class="align-self-center">bis</span>
<select class="form-select ms-2" id="seasonEnd">
<option value="1">1</option>
</select>
</div>
</div>
<div class="col-md-6 d-none" id="movieContainer">
<label class="form-label">Film</label>
<div class="d-flex">
<select class="form-select me-2" id="movieStart">
<option value="1">1</option>
</select>
<span class="align-self-center">bis</span>
<select class="form-select ms-2" id="movieEnd">
<option value="1">1</option>
</select>
</div>
</div>
</div>
<div class="row mb-3" id="episodeContainer">
<div class="col-md-6">
<label class="form-label">Episode</label>
<div class="d-flex">
<select class="form-select me-2" id="episodeStart">
<option value="1">1</option>
</select>
<span class="align-self-center">bis</span>
<select class="form-select ms-2" id="episodeEnd">
<option value="1">1</option>
</select>
</div>
</div>
<div class="col-md-6">
<label class="form-label">Nach Download</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="reconnectVpn" checked>
<label class="form-check-label" for="reconnectVpn">
VPN neu verbinden
</label>
</div>
</div>
</div>
<div class="d-grid mt-4">
<button class="btn btn-success" id="startDownloadBtn">
<i class="bi bi-cloud-download"></i> Download starten
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-arrow-down-circle"></i> Aktuelle Downloads</h5>
<button class="btn btn-sm btn-outline-secondary" id="refreshDownloads">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="card-body" id="currentDownloads">
<div class="text-center py-4 text-muted">
<i class="bi bi-cloud-slash display-4"></i>
<p class="mt-2">Keine aktiven Downloads</p>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Schnellsuche</h5>
</div>
<div class="card-body">
<form action="{{ url_for('search') }}" method="get">
<div class="input-group">
<input type="text" class="form-control" name="q" placeholder="Suchbegriff...">
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize variables
let contentData = null;
// Event listeners
document.getElementById('analyzeButton').addEventListener('click', analyzeLink);
document.getElementById('startDownloadBtn').addEventListener('click', startDownload);
document.getElementById('refreshDownloads').addEventListener('click', updateCurrentDownloads);
document.getElementById('contentType').addEventListener('change', toggleContentType);
// Initialize content type view
function toggleContentType() {
const contentType = document.getElementById('contentType').value;
if (contentType === 'episodes') {
document.getElementById('seasonContainer').classList.remove('d-none');
document.getElementById('episodeContainer').classList.remove('d-none');
document.getElementById('movieContainer').classList.add('d-none');
} else {
document.getElementById('seasonContainer').classList.add('d-none');
document.getElementById('episodeContainer').classList.add('d-none');
document.getElementById('movieContainer').classList.remove('d-none');
}
}
// Load data from server
async function analyzeLink() {
const link = document.getElementById('animeLink').value;
if (!link) {
alert('Bitte gib einen Link ein.');
return;
}
try {
const response = await fetch('/api/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
link: link,
content_type: document.getElementById('contentType').value
})
});
if (!response.ok) {
throw new Error('Fehler beim Analysieren des Links');
}
contentData = await response.json();
// Update UI
document.getElementById('contentTitle').textContent = contentData.title;
document.getElementById('linkAnalysis').classList.remove('d-none');
if (contentData.content_type === 'episodes') {
// Update season options
updateSelectOptions('seasonStart', 1, contentData.seasons);
updateSelectOptions('seasonEnd', 1, contentData.seasons);
// Update episode options based on first season
updateSelectOptions('episodeStart', 1, contentData.episodes[0]);
updateSelectOptions('episodeEnd', 1, contentData.episodes[0]);
// Set event listeners for season changes
document.getElementById('seasonStart').addEventListener('change', updateEpisodeStartOptions);
document.getElementById('seasonEnd').addEventListener('change', updateEpisodeEndOptions);
} else {
// Update movie options
updateSelectOptions('movieStart', 1, contentData.movies);
updateSelectOptions('movieEnd', 1, contentData.movies);
}
toggleContentType();
} catch (error) {
console.error('Error:', error);
alert('Fehler beim Analysieren: ' + error.message);
}
}
function updateSelectOptions(elementId, start, end) {
const select = document.getElementById(elementId);
select.innerHTML = '';
for (let i = start; i <= end; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = i;
select.appendChild(option);
}
// If end options, select the last one
if (elementId.includes('End')) {
select.value = end;
}
}
function updateEpisodeStartOptions() {
const seasonIndex = parseInt(document.getElementById('seasonStart').value) - 1;
if (contentData && contentData.episodes && contentData.episodes[seasonIndex]) {
updateSelectOptions('episodeStart', 1, contentData.episodes[seasonIndex]);
}
}
function updateEpisodeEndOptions() {
const seasonIndex = parseInt(document.getElementById('seasonEnd').value) - 1;
if (contentData && contentData.episodes && contentData.episodes[seasonIndex]) {
updateSelectOptions('episodeEnd', 1, contentData.episodes[seasonIndex]);
document.getElementById('episodeEnd').value = contentData.episodes[seasonIndex];
}
}
async function startDownload() {
if (!contentData) {
alert('Bitte analysiere zuerst einen Link.');
return;
}
const contentType = document.getElementById('contentType').value;
const requestData = {
link: contentData.link,
provider: document.getElementById('provider').value,
language: document.getElementById('language').value,
content_type: contentType
};
if (contentType === 'episodes') {
requestData.season_start = parseInt(document.getElementById('seasonStart').value);
requestData.season_end = parseInt(document.getElementById('seasonEnd').value);
requestData.episode_start = parseInt(document.getElementById('episodeStart').value);
requestData.episode_end = parseInt(document.getElementById('episodeEnd').value);
} else {
requestData.movie_start = parseInt(document.getElementById('movieStart').value);
requestData.movie_end = parseInt(document.getElementById('movieEnd').value);
}
try {
const response = await fetch('/api/download/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error('Fehler beim Starten des Downloads');
}
const result = await response.json();
if (result.success) {
alert(`${result.downloads_added} Downloads wurden zur Warteschlange hinzugefügt!`);
updateCurrentDownloads();
} else {
alert('Fehler beim Hinzufügen des Downloads.');
}
} catch (error) {
console.error('Error:', error);
alert('Fehler beim Starten des Downloads: ' + error.message);
}
}
async function updateCurrentDownloads() {
try {
const response = await fetch('/api/downloads');
if (!response.ok) {
throw new Error('Fehler beim Laden der Downloads');
}
const downloads = await response.json();
const container = document.getElementById('currentDownloads');
const activeDownloads = downloads.filter(d =>
d.status === 'downloading' || d.status === 'queued'
);
if (activeDownloads.length === 0) {
container.innerHTML = `
<div class="text-center py-4 text-muted">
<i class="bi bi-cloud-slash display-4"></i>
<p class="mt-2">Keine aktiven Downloads</p>
</div>
`;
} else {
container.innerHTML = '';
activeDownloads.forEach(download => {
const card = document.createElement('div');
card.className = 'download-item mb-3 p-2 border-bottom';
const title = download.season
? `${download.title} S${download.season}E${download.episode}`
: `${download.title} Film ${download.episode}`;
const statusClass = download.status === 'downloading'
? 'text-primary'
: 'text-secondary';
card.innerHTML = `
<div class="d-flex justify-content-between">
<small title="${title}">${truncateText(title, 28)}</small>
<small class="${statusClass}">${capitalizeFirst(download.status)}</small>
</div>
<div class="progress mt-1" style="height: 6px;">
<div class="progress-bar" role="progressbar" style="width: ${download.progress}%"
aria-valuenow="${download.progress}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between mt-1">
<small class="text-muted">${download.language}</small>
<small class="text-muted">${download.provider}</small>
</div>
`;
container.appendChild(card);
});
}
} catch (error) {
console.error('Error:', error);
}
}
function truncateText(text, maxLength) {
return text.length > maxLength ? text.substring(0, maxLength) + '...' : text;
}
function capitalizeFirst(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// Initial load of current downloads
updateCurrentDownloads();
// Auto-update downloads every 5 seconds
setInterval(updateCurrentDownloads, 5000);
});
</script>
{% endblock %}

87
templates/login.html Normal file
View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}Anmeldung - Media Manager{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/login.css">
{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2><i class="bi bi-shield-lock"></i> Anmeldung</h2>
<p class="text-muted">Bitte melden Sie sich mit Ihren Zugangsdaten an</p>
</div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<div id="error-message" class="alert alert-danger d-none"></div>
<button type="submit" class="btn btn-primary w-100" id="loginBtn">
<span class="spinner-border spinner-border-sm d-none me-2" id="loginSpinner"></span>
Anmelden
</button>
</form>
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const loginBtn = document.getElementById('loginBtn');
const loginSpinner = document.getElementById('loginSpinner');
const errorDiv = document.getElementById('error-message');
// UI während Login aktualisieren
loginBtn.disabled = true;
loginSpinner.classList.remove('d-none');
errorDiv.classList.add('d-none');
const formData = {
username: document.getElementById('username').value,
password: document.getElementById('password').value
};
try {
const response = await fetch('/api/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
window.location.href = data.redirect || '/';
} else {
throw new Error(data.error || 'Anmeldung fehlgeschlagen');
}
} catch (error) {
errorDiv.textContent = error.message;
errorDiv.classList.remove('d-none');
} finally {
loginBtn.disabled = false;
loginSpinner.classList.add('d-none');
}
});
</script>
{% endblock %}

35
templates/logs.html Normal file
View File

@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Anmelden - AniWorld Downloader{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">Mit Jellyfin anmelden</h4>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right"></i> Anmelden
</button>
</form>
<div class="mt-4 text-center">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i> Du musst einen gültigen Jellyfin-Account haben, um den Downloader zu nutzen.
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

157
templates/queue.html Normal file
View File

@ -0,0 +1,157 @@
{% extends "base.html" %}
{% block title %}Warteschlange - AniWorld Downloader{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-list-check"></i> Download-Warteschlange</h2>
<button class="btn btn-outline-secondary" id="refreshQueueBtn">
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
</button>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Titel</th>
<th>S/E</th>
<th>Provider</th>
<th>Sprache</th>
<th>Status</th>
<th>Fortschritt</th>
<th>Erstellt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="downloadTable">
{% for download in downloads %}
<tr data-download-id="{{ download.id }}">
<td>{{ download.title }}</td>
<td>
{% if download.season %}
S{{ download.season }}E{{ download.episode }}
{% else %}
Film {{ download.episode }}
{% endif %}
</td>
<td>{{ download.provider }}</td>
<td>{{ download.language }}</td>
<td>
<span class="badge bg-{% if download.status == 'completed' %}success{% elif download.status == 'downloading' %}primary{% elif download.status == 'failed' %}danger{% elif download.status == 'cancelled' %}warning{% else %}secondary{% endif %}">
{{ download.status.title() }}
</span>
</td>
<td>
<div class="progress" style="width: 100px; height: 10px;">
<div class="progress-bar {% if download.status == 'failed' %}bg-danger{% elif download.status == 'cancelled' %}bg-warning{% endif %}"
style="width: {{ download.progress }}%"
aria-valuenow="{{ download.progress }}"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
<small class="text-muted">{{ download.progress|round(1) }}%</small>
</td>
<td>{{ download.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
<td>
{% if download.status in ['queued', 'downloading'] %}
<button class="btn btn-sm btn-outline-danger cancel-btn"
data-download-id="{{ download.id }}">
<i class="bi bi-x-circle"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if not downloads %}
<div class="text-center py-5 text-muted">
<i class="bi bi-inbox display-4"></i>
<p class="mt-3">Keine Downloads in der Warteschlange</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Refresh button
document.getElementById('refreshQueueBtn').addEventListener('click', function() {
location.reload();
});
// Cancel buttons
document.querySelectorAll('.cancel-btn').forEach(button => {
button.addEventListener('click', async function() {
const downloadId = this.getAttribute('data-download-id');
if (confirm('Download wirklich abbrechen?')) {
try {
const response = await fetch(`/api/download/${downloadId}/cancel`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
if (result.success) {
// Update status in table
const row = document.querySelector(`tr[data-download-id="${downloadId}"]`);
const statusCell = row.querySelector('td:nth-child(5) span');
statusCell.className = 'badge bg-warning';
statusCell.textContent = 'Cancelled';
// Remove cancel button
row.querySelector('td:last-child button').remove();
}
} else {
alert('Fehler beim Abbrechen des Downloads');
}
} catch (error) {
console.error('Error:', error);
alert('Fehler beim Abbrechen des Downloads');
}
}
});
});
// Auto-update progress for downloading items
setInterval(async function() {
const downloadingRows = document.querySelectorAll('tr[data-download-id] td:nth-child(5) span.badge.bg-primary');
for (const statusBadge of downloadingRows) {
const row = statusBadge.closest('tr');
const downloadId = row.getAttribute('data-download-id');
try {
const response = await fetch(`/api/download/${downloadId}/status`);
if (response.ok) {
const data = await response.json();
// Update progress bar
const progressBar = row.querySelector('.progress-bar');
progressBar.style.width = `${data.progress}%`;
progressBar.setAttribute('aria-valuenow', data.progress);
// Update progress text
row.querySelector('td:nth-child(6) small').textContent = `${data.progress.toFixed(1)}%`;
// Update status if changed
if (data.status !== 'downloading') {
// Reload the page to show updated status
location.reload();
}
}
} catch (error) {
console.error(`Error updating download ${downloadId}:`, error);
}
}
}, 3000);
});
</script>
{% endblock %}