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