Dateien nach "templates" hochladen
HTML
This commit is contained in:
parent
7dd35b89fb
commit
278422f934
84
templates/base.html
Normal file
84
templates/base.html
Normal 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
392
templates/index.html
Normal 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
87
templates/login.html
Normal 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
35
templates/logs.html
Normal 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
157
templates/queue.html
Normal 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 %}
|
||||
Loading…
Reference in New Issue
Block a user