55bb85b56f
Change the 'Nettoyer' (Cleanup) button behavior to remove unwanted downloads: Before: - Only removed completed downloads Now: - Removes cancelled downloads - Removes failed downloads - Removes completed downloads - Keeps only: downloading, paused, pending downloads - Shows detailed count by status in confirmation dialog - Better title tooltip explaining what gets deleted The confirmation message now shows exactly what will be deleted: - 'Supprimer X annulé(s) Y échoué(s) Z terminé(s)?' This makes it easy to clean up the download history while keeping active downloads safe.
1763 lines
66 KiB
HTML
1763 lines
66 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ohm Stream Downloader</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
min-height: 100vh;
|
|
color: #eee;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
margin-bottom: 10px;
|
|
font-size: 2.5em;
|
|
background: linear-gradient(45deg, #00d9ff, #00ff88);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
color: #888;
|
|
margin-bottom: 30px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.url-form {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.input-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
input[type="text"] {
|
|
flex: 1;
|
|
padding: 12px 15px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: #fff;
|
|
font-size: 14px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: #00d9ff;
|
|
box-shadow: 0 0 0 3px rgba(0, 217, 255, 0.1);
|
|
}
|
|
|
|
button {
|
|
padding: 12px 25px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
button svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.btn-small svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(45deg, #00d9ff, #00ff88);
|
|
color: #000;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 20px rgba(0, 217, 255, 0.4);
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.btn-pause {
|
|
background: #ffa500;
|
|
color: #000;
|
|
}
|
|
|
|
.btn-resume {
|
|
background: #00ff88;
|
|
color: #000;
|
|
}
|
|
|
|
.btn-cancel {
|
|
background: #ff4444;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-download {
|
|
background: #00d9ff;
|
|
color: #000;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
margin-top: 40px;
|
|
}
|
|
|
|
.section-header h2 {
|
|
font-size: 1.8em;
|
|
margin: 0;
|
|
background: linear-gradient(45deg, #00d9ff, #00ff88);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.downloads-stats {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.stat-item {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
padding: 5px 12px;
|
|
border-radius: 15px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.stat-count {
|
|
font-weight: bold;
|
|
color: #00d9ff;
|
|
}
|
|
|
|
.downloads-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.filter-group label {
|
|
font-size: 0.85em;
|
|
color: #aaa;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.filter-group select,
|
|
.filter-group input {
|
|
padding: 8px 12px;
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 6px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.filter-group select:focus,
|
|
.filter-group input:focus {
|
|
outline: none;
|
|
border-color: #00d9ff;
|
|
}
|
|
|
|
.search-group input {
|
|
min-width: 200px;
|
|
}
|
|
|
|
.actions-group {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.downloads-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.downloads-group {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.downloads-group-header {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
padding: 12px 18px;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: all 0.3s;
|
|
position: relative;
|
|
}
|
|
|
|
.downloads-group-header:hover {
|
|
background: rgba(255, 255, 255, 0.12);
|
|
}
|
|
|
|
.downloads-group-header::before {
|
|
content: '▼';
|
|
position: absolute;
|
|
right: 18px;
|
|
font-size: 0.8em;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.downloads-group-header.collapsed::before {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.downloads-group-title {
|
|
font-weight: 600;
|
|
font-size: 1.05em;
|
|
color: #00d9ff;
|
|
padding-right: 30px;
|
|
}
|
|
|
|
.downloads-group-count {
|
|
background: rgba(0, 217, 255, 0.2);
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 0.85em;
|
|
color: #00d9ff;
|
|
}
|
|
|
|
.downloads-group-items {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.download-item {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.download-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.filename {
|
|
font-weight: 600;
|
|
color: #00d9ff;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.status {
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-pending { background: #666; }
|
|
.status-downloading { background: #00d9ff; color: #000; }
|
|
.status-paused { background: #ffa500; color: #000; }
|
|
.status-completed { background: #00ff88; color: #000; }
|
|
.status-failed { background: #ff4444; }
|
|
.status-cancelled { background: #999; }
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 8px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #00d9ff, #00ff88);
|
|
transition: width 0.3s;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.download-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
color: #888;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.download-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.url-display {
|
|
font-size: 11px;
|
|
color: #666;
|
|
word-break: break-all;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.error-message {
|
|
color: #ff4444;
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #666;
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 80px;
|
|
height: 80px;
|
|
margin-bottom: 20px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.supported-hosts {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.host-badge {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
font-size: 11px;
|
|
color: #888;
|
|
}
|
|
|
|
/* Tabs */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.tab {
|
|
padding: 10px 20px;
|
|
background: transparent;
|
|
color: #888;
|
|
border: none;
|
|
border-bottom: 2px solid transparent;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.tab:hover {
|
|
color: #00d9ff;
|
|
}
|
|
|
|
.tab.active {
|
|
color: #00d9ff;
|
|
border-bottom-color: #00d9ff;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.anime-input-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.anime-select-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
select {
|
|
padding: 12px 15px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: #fff;
|
|
font-size: 14px;
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
select:focus {
|
|
outline: none;
|
|
border-color: #00d9ff;
|
|
}
|
|
|
|
.search-results {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.anime-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
transition: all 0.3s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.anime-card:hover {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border-color: rgba(0, 217, 255, 0.3);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.anime-card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.anime-card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
}
|
|
|
|
.anime-card-provider {
|
|
font-size: 12px;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
background: rgba(0, 217, 255, 0.2);
|
|
color: #00d9ff;
|
|
}
|
|
|
|
.anime-card-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.anime-card-actions select {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 6px;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.anime-card-actions button {
|
|
flex: 1;
|
|
padding: 8px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.anime-metadata {
|
|
font-size: 12px;
|
|
color: #aaa;
|
|
margin-bottom: 10px;
|
|
padding: 8px 12px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 6px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.anime-synopsis {
|
|
margin-bottom: 10px;
|
|
padding: 10px 12px;
|
|
background: rgba(0, 217, 255, 0.05);
|
|
border-left: 3px solid #00d9ff;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.anime-synopsis summary {
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #00d9ff;
|
|
margin-bottom: 8px;
|
|
user-select: none;
|
|
}
|
|
|
|
.anime-synopsis summary:hover {
|
|
color: #00ff88;
|
|
}
|
|
|
|
.anime-synopsis p {
|
|
font-size: 12px;
|
|
color: #ccc;
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.loading-spinner {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #888;
|
|
}
|
|
|
|
.loading-spinner::after {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 30px;
|
|
height: 30px;
|
|
border: 3px solid rgba(0, 217, 255, 0.3);
|
|
border-top-color: #00d9ff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin-left: 10px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.no-results {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #888;
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.8em;
|
|
}
|
|
|
|
.container {
|
|
max-width: 100%;
|
|
}
|
|
|
|
.url-form {
|
|
padding: 15px;
|
|
}
|
|
|
|
.input-group {
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.btn-primary {
|
|
width: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.download-item {
|
|
padding: 15px;
|
|
}
|
|
|
|
.filename {
|
|
font-size: 14px;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.download-header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
}
|
|
|
|
.status {
|
|
align-self: flex-start;
|
|
}
|
|
|
|
.download-actions {
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 8px 10px;
|
|
font-size: 10px;
|
|
flex: 1 1 auto;
|
|
min-width: 80px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.download-info {
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.supported-hosts {
|
|
gap: 6px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
h1 {
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.btn-small {
|
|
min-width: 70px;
|
|
padding: 6px 8px;
|
|
font-size: 9px;
|
|
}
|
|
|
|
.btn-small svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>⚡ Ohm Stream Downloader</h1>
|
|
<p class="subtitle">Téléchargez vos vidéos et animes depuis vos hébergeurs préférés</p>
|
|
|
|
<!-- Tabs (Anime providers will be added dynamically) -->
|
|
<div class="tabs">
|
|
<button class="tab active" data-tab-type="search" onclick="switchTab('search')">
|
|
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
Recherche
|
|
</button>
|
|
<button class="tab" data-tab-type="direct" onclick="switchTab('direct')">
|
|
<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
|
</svg>
|
|
Lien direct
|
|
</button>
|
|
<!-- Anime provider tabs will be loaded dynamically -->
|
|
</div>
|
|
|
|
<!-- Search Tab -->
|
|
<div id="tab-search" class="tab-content active">
|
|
<div class="url-form">
|
|
<div class="input-group">
|
|
<input
|
|
type="text"
|
|
id="searchInput"
|
|
placeholder="Rechercher un anime..."
|
|
onkeypress="if(event.key === 'Enter') searchAnime()"
|
|
>
|
|
<select id="langSelect" style="max-width: 120px;">
|
|
<option value="vostfr">VOSTFR</option>
|
|
<option value="vf">VF</option>
|
|
</select>
|
|
<button type="button" class="btn-primary" onclick="searchAnime()">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
Rechercher
|
|
</button>
|
|
</div>
|
|
<div style="display: flex; align-items: center; gap: 10px; margin-top: 10px; font-size: 13px; color: #888;">
|
|
<input type="checkbox" id="includeMetadata" style="width: auto; margin: 0;">
|
|
<label for="includeMetadata" style="cursor: pointer; user-select: none;">
|
|
📊 Inclure les métadonnées (synopsis, genres, note) • Plus lent mais plus complet
|
|
</label>
|
|
</div>
|
|
<div style="margin-top: 10px; padding: 10px; background: rgba(0, 217, 255, 0.1); border-radius: 8px; font-size: 12px; color: #88;">
|
|
💡 <strong>Astuce:</strong> Pour de meilleurs résultats, essayez le nom en anglais ou japonais (ex: "One Piece", "Naruto"). Certains sites n'ont pas tous les animes.
|
|
</div>
|
|
</div>
|
|
|
|
<div id="searchResults" class="search-results"></div>
|
|
</div>
|
|
|
|
<!-- Direct Download Tab -->
|
|
<div id="tab-direct" class="tab-content">
|
|
<div class="url-form">
|
|
<form id="downloadForm">
|
|
<div class="input-group">
|
|
<input
|
|
type="text"
|
|
id="urlInput"
|
|
placeholder="Collez le lien de téléchargement ici..."
|
|
required
|
|
>
|
|
<button type="submit" class="btn-primary">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
Télécharger
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<div class="supported-hosts">
|
|
<span class="host-badge">1fichier</span>
|
|
<span class="host-badge">Doodstream</span>
|
|
<span class="host-badge">Rapidfile</span>
|
|
<span class="host-badge">Anime-Sama</span>
|
|
<span class="host-badge">Anime-Ultime</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Downloads Section with Filters -->
|
|
<div class="section-header">
|
|
<h2>Téléchargements</h2>
|
|
<div class="downloads-stats" id="downloadsStats"></div>
|
|
</div>
|
|
|
|
<!-- Filters and Controls -->
|
|
<div class="downloads-controls">
|
|
<div class="filter-group">
|
|
<label>Statut:</label>
|
|
<select id="statusFilter" onchange="filterDownloads()">
|
|
<option value="all">Tous</option>
|
|
<option value="downloading">En cours</option>
|
|
<option value="paused">En pause</option>
|
|
<option value="completed">Terminés</option>
|
|
<option value="cancelled">Annulés</option>
|
|
<option value="failed">Échoués</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label>Tri par:</label>
|
|
<select id="sortBy" onchange="filterDownloads()">
|
|
<option value="date">Date (récent)</option>
|
|
<option value="date_asc">Date (ancien)</option>
|
|
<option value="name">Nom (A-Z)</option>
|
|
<option value="name_desc">Nom (Z-A)</option>
|
|
<option value="size">Taille</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label>Regroupement:</label>
|
|
<select id="groupBy" onchange="filterDownloads()">
|
|
<option value="none">Aucun</option>
|
|
<option value="series">Par série</option>
|
|
<option value="status">Par statut</option>
|
|
<option value="day">Par jour</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group search-group">
|
|
<input type="text" id="searchDownloads" placeholder="🔍 Rechercher..." oninput="filterDownloads()">
|
|
</div>
|
|
|
|
<div class="actions-group">
|
|
<button class="btn-small btn-secondary" onclick="clearCompleted()" title="Supprimer annulés, échoués et terminés">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
Nettoyer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="downloadsList" class="downloads-list">
|
|
<div class="empty-state">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
|
|
</svg>
|
|
<p>Aucun téléchargement pour le moment</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
let autoRefreshInterval;
|
|
let currentAnimeUrl = '';
|
|
let searchResultsCache = {};
|
|
let allDownloads = []; // Store all downloads for filtering
|
|
let collapsedGroups = new Set(); // Store which groups are collapsed
|
|
|
|
// Extract series name from filename (for grouping)
|
|
function extractSeriesName(filename) {
|
|
// Common patterns:
|
|
// - "Naruto Shippuden - Episode 123.mp4"
|
|
// - "[One Piece] Ep 456.mkv"
|
|
// - "Attack on Titan S03E09.mp4"
|
|
// - "Anime Name - S01E05.mp4"
|
|
|
|
let name = filename;
|
|
|
|
// Remove file extension
|
|
name = name.replace(/\.[^/.]+$/, '');
|
|
|
|
// Remove episode numbers and patterns
|
|
name = name
|
|
.replace(/[-_ ]?(E(?:p)?|Episode|Épisode|Saison|Season)[-_: ]?\d+/gi, '')
|
|
.replace(/[-_ ]?S\d{2}E\d{2}/gi, '')
|
|
.replace(/\[.*?\]/g, '')
|
|
.replace(/\(.*\)/g, '')
|
|
.replace(/[-_ ]?\d{3,4}p/gi, '')
|
|
.replace(/[-_ ]?(VOSTFR|VF|MULTI)/gi, '')
|
|
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
|
|
.replace(/[-_]+$/, '') // Remove trailing dashes/underscores
|
|
.trim();
|
|
|
|
// If nothing left or too short, use original filename without extension
|
|
if (!name || name.length < 3) {
|
|
return filename.replace(/\.[^/.]+$/, '');
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
// Get day string for grouping
|
|
function getDayString(dateString) {
|
|
const date = new Date(dateString);
|
|
const today = new Date();
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
if (date.toDateString() === today.toDateString()) {
|
|
return "Aujourd'hui";
|
|
} else if (date.toDateString() === yesterday.toDateString()) {
|
|
return "Hier";
|
|
} else {
|
|
return date.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'short' });
|
|
}
|
|
}
|
|
|
|
// Filter and sort downloads
|
|
function filterDownloads() {
|
|
const statusFilter = document.getElementById('statusFilter').value;
|
|
const sortBy = document.getElementById('sortBy').value;
|
|
const groupBy = document.getElementById('groupBy').value;
|
|
const searchTerm = document.getElementById('searchDownloads').value.toLowerCase();
|
|
|
|
// Clear collapsed groups when filters change (optional)
|
|
// collapsedGroups.clear();
|
|
|
|
// Filter by status and search
|
|
let filtered = allDownloads.filter(dl => {
|
|
const matchesStatus = statusFilter === 'all' || dl.status === statusFilter;
|
|
const matchesSearch = !searchTerm ||
|
|
dl.filename.toLowerCase().includes(searchTerm) ||
|
|
(dl.url && dl.url.toLowerCase().includes(searchTerm));
|
|
return matchesStatus && matchesSearch;
|
|
});
|
|
|
|
// Sort
|
|
filtered.sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'date_asc':
|
|
return new Date(a.created_at) - new Date(b.created_at);
|
|
case 'name':
|
|
return a.filename.localeCompare(b.filename);
|
|
case 'name_desc':
|
|
return b.filename.localeCompare(a.filename);
|
|
case 'size':
|
|
return (b.total_bytes || 0) - (a.total_bytes || 0);
|
|
case 'date':
|
|
default:
|
|
return new Date(b.created_at) - new Date(a.created_at);
|
|
}
|
|
});
|
|
|
|
// Apply grouping
|
|
displayDownloads(filtered, groupBy);
|
|
}
|
|
|
|
// Group downloads
|
|
function groupDownloads(downloads, groupBy) {
|
|
const groups = {};
|
|
|
|
downloads.forEach(dl => {
|
|
let key = 'Ungrouped';
|
|
|
|
switch (groupBy) {
|
|
case 'series':
|
|
key = extractSeriesName(dl.filename);
|
|
break;
|
|
case 'status':
|
|
key = translateStatus(dl.status);
|
|
break;
|
|
case 'day':
|
|
key = getDayString(dl.created_at);
|
|
break;
|
|
default:
|
|
key = 'Tous';
|
|
}
|
|
|
|
if (!groups[key]) {
|
|
groups[key] = [];
|
|
}
|
|
groups[key].push(dl);
|
|
});
|
|
|
|
return groups;
|
|
}
|
|
|
|
// Clear unwanted downloads (cancelled, deleted, failed)
|
|
async function clearCompleted() {
|
|
// Find all unwanted downloads (not completed or downloading or paused)
|
|
const unwanted = allDownloads.filter(dl =>
|
|
dl.status === 'cancelled' ||
|
|
dl.status === 'failed' ||
|
|
dl.status === 'completed' // Also remove completed if desired
|
|
);
|
|
|
|
if (unwanted.length === 0) {
|
|
alert('Aucun téléchargement à supprimer');
|
|
return;
|
|
}
|
|
|
|
// Count by status
|
|
const byStatus = unwanted.reduce((acc, dl) => {
|
|
acc[dl.status] = (acc[dl.status] || 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
|
|
let message = 'Supprimer ';
|
|
if (byStatus.cancelled) message += `${byStatus.cancelled} annulé(s) `;
|
|
if (byStatus.failed) message += `${byStatus.failed} échoué(s) `;
|
|
if (byStatus.completed) message += `${byStatus.completed} terminé(s) `;
|
|
message += '?';
|
|
|
|
if (!confirm(message)) {
|
|
return;
|
|
}
|
|
|
|
for (const dl of unwanted) {
|
|
await fetch(`${API_BASE}/download/${dl.id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
loadDownloads();
|
|
}
|
|
|
|
// Search Anime across all providers
|
|
async function searchAnime() {
|
|
const query = document.getElementById('searchInput').value.trim();
|
|
const lang = document.getElementById('langSelect').value;
|
|
const includeMetadata = document.getElementById('includeMetadata').checked;
|
|
|
|
if (!query) {
|
|
alert('Veuillez entrer un nom d\'anime');
|
|
return;
|
|
}
|
|
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
resultsContainer.innerHTML = '<div class="loading-spinner">Recherche en cours...</div>';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/search?q=${encodeURIComponent(query)}&lang=${lang}&include_metadata=${includeMetadata}`);
|
|
const data = await response.json();
|
|
|
|
displaySearchResults(data, lang);
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
resultsContainer.innerHTML = '<div class="no-results">Erreur lors de la recherche</div>';
|
|
}
|
|
}
|
|
|
|
async function displaySearchResults(data, lang) {
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
const providers = await getProvidersInfo();
|
|
|
|
let totalResults = 0;
|
|
let html = '';
|
|
|
|
for (const [providerId, results] of Object.entries(data.results)) {
|
|
if (results && results.length > 0) {
|
|
totalResults += results.length;
|
|
|
|
results.forEach(anime => {
|
|
const providerInfo = providers.anime_providers[providerId];
|
|
|
|
// Build metadata HTML if available
|
|
let metadataHtml = '';
|
|
if (anime.metadata) {
|
|
const meta = anime.metadata;
|
|
let metaParts = [];
|
|
|
|
if (meta.release_year) metaParts.push(`📅 ${meta.release_year}`);
|
|
if (meta.rating) metaParts.push(`⭐ ${meta.rating}`);
|
|
if (meta.genres && meta.genres.length > 0) metaParts.push(`🏷️ ${meta.genres.slice(0, 3).join(', ')}`);
|
|
if (meta.total_episodes) metaParts.push(`📺 ${meta.total_episodes} épisodes`);
|
|
if (meta.status) metaParts.push(`📡 ${meta.status === 'Ongoing' ? 'En cours' : 'Terminé'}`);
|
|
|
|
if (metaParts.length > 0) {
|
|
metadataHtml = `
|
|
<div class="anime-metadata">
|
|
${metaParts.join(' • ')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Add synopsis if available (expandable)
|
|
if (meta.synopsis) {
|
|
metadataHtml += `
|
|
<details class="anime-synopsis">
|
|
<summary>📖 Synopsis</summary>
|
|
<p>${escapeHtml(meta.synopsis)}</p>
|
|
</details>
|
|
`;
|
|
}
|
|
}
|
|
|
|
html += `
|
|
<div class="anime-card" id="anime-${providerId}-${encodeURIComponent(anime.url)}">
|
|
<div class="anime-card-header">
|
|
<div class="anime-card-title">${escapeHtml(anime.title)}</div>
|
|
<div class="anime-card-provider">${providerInfo?.icon || ''} ${providerInfo?.name || providerId}</div>
|
|
</div>
|
|
${metadataHtml}
|
|
<div class="anime-card-actions">
|
|
<select id="episodes-${providerId}-${encodeURIComponent(anime.url)}">
|
|
<option value="">Charger les épisodes...</option>
|
|
</select>
|
|
</div>
|
|
<div class="anime-card-actions" id="actions-${providerId}-${encodeURIComponent(anime.url)}" style="display:none;">
|
|
<button class="btn-primary" onclick="downloadAnimeEpisode('${encodeURIComponent(anime.url)}', '${providerId}', '${lang}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;margin-right:4px;">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
Télécharger
|
|
</button>
|
|
<button class="btn-primary" style="background: linear-gradient(45deg, #ff6b6b, #ffa500);" onclick="downloadEntireSeason('${encodeURIComponent(anime.url)}', '${lang}')" title="Télécharger toute la saison">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:14px;height:14px;margin-right:4px;">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
|
|
</svg>
|
|
Toute la saison
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Auto-load episodes
|
|
setTimeout(() => {
|
|
loadEpisodesForAnime(providerId, encodeURIComponent(anime.url), lang, null);
|
|
}, 100);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (totalResults === 0) {
|
|
html = '<div class="no-results">Aucun résultat trouvé</div>';
|
|
}
|
|
|
|
resultsContainer.innerHTML = html;
|
|
}
|
|
|
|
async function loadEpisodesForAnime(providerId, encodedUrl, lang, selectElement) {
|
|
const url = decodeURIComponent(encodedUrl);
|
|
const selectId = `episodes-${providerId}-${encodedUrl}`;
|
|
const actionsId = `actions-${providerId}-${encodedUrl}`;
|
|
|
|
if (!selectElement) {
|
|
selectElement = document.getElementById(selectId);
|
|
}
|
|
|
|
if (!selectElement) return;
|
|
|
|
selectElement.innerHTML = '<option value="">Chargement...</option>';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(url)}&lang=${lang}`);
|
|
const data = await response.json();
|
|
|
|
if (data.episodes && data.episodes.length > 0) {
|
|
selectElement.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
|
data.episodes.forEach(ep => {
|
|
const option = document.createElement('option');
|
|
option.value = ep.url;
|
|
option.textContent = `Épisode ${ep.episode}`;
|
|
selectElement.appendChild(option);
|
|
});
|
|
|
|
// Show download buttons immediately (season button is always useful)
|
|
const actionsDiv = document.getElementById(actionsId);
|
|
actionsDiv.style.display = 'flex';
|
|
|
|
// Store the selected episode URL for download
|
|
selectElement.dataset.animeUrl = url;
|
|
selectElement.dataset.providerId = providerId;
|
|
selectElement.dataset.lang = lang;
|
|
} else {
|
|
selectElement.innerHTML = '<option value="">Aucun épisode disponible</option>';
|
|
selectElement.disabled = true;
|
|
// Add warning message
|
|
const card = document.getElementById(`anime-${providerId}-${encodedUrl}`);
|
|
if (card) {
|
|
const warning = document.createElement('div');
|
|
warning.style.cssText = 'margin-top: 10px; padding: 10px; background: rgba(255, 100, 100, 0.2); border-radius: 6px; font-size: 12px; color: #ff6b6b;';
|
|
warning.textContent = '⚠️ Aucun épisode trouvé. Essayez une recherche exacte ou un autre fournisseur.';
|
|
card.appendChild(warning);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading episodes:', error);
|
|
selectElement.innerHTML = '<option value="">Erreur de chargement</option>';
|
|
}
|
|
}
|
|
|
|
async function downloadAnimeEpisode(encodedUrl, providerId, lang) {
|
|
const url = decodeURIComponent(encodedUrl);
|
|
const selectId = `episodes-${providerId}-${encodedUrl}`;
|
|
const selectElement = document.getElementById(selectId);
|
|
|
|
const episodeUrl = selectElement.value;
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Show success message and refresh downloads
|
|
loadDownloads();
|
|
alert('Téléchargement démarré!');
|
|
|
|
// Keep the select available for more downloads, just reset the selection
|
|
selectElement.value = '';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Erreur lors du démarrage du téléchargement');
|
|
}
|
|
}
|
|
|
|
async function downloadEntireSeason(encodedUrl, lang) {
|
|
const url = decodeURIComponent(encodedUrl);
|
|
|
|
if (!confirm(`⚠️ Attention: Vous allez télécharger toute la saison. Cela peut prendre du temps et utiliser beaucoup d'espace disque.\n\nVoulez-vous continuer ?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download-season?url=${encodeURIComponent(url)}&lang=${lang}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
loadDownloads();
|
|
alert(`✅ ${data.message}\n\n${data.total_episodes} épisodes ont été ajoutés à la file de téléchargement!`);
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Erreur: ${error.detail || 'Impossible de démarrer le téléchargement de la saison'}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Erreur lors du démarrage du téléchargement de la saison');
|
|
}
|
|
}
|
|
|
|
async function getProvidersInfo() {
|
|
if (!searchResultsCache.providers) {
|
|
const response = await fetch(`${API_BASE}/providers`);
|
|
searchResultsCache.providers = await response.json();
|
|
}
|
|
return searchResultsCache.providers;
|
|
}
|
|
|
|
// Load providers dynamically
|
|
async function loadProviders() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/providers`);
|
|
const data = await response.json();
|
|
|
|
// Update anime tabs
|
|
const animeTabsContainer = document.querySelector('.tabs');
|
|
const existingTabs = animeTabsContainer.querySelectorAll('.tab[data-tab-type="anime"]');
|
|
existingTabs.forEach(tab => tab.remove());
|
|
|
|
// Add anime provider tabs
|
|
Object.entries(data.anime_providers).forEach(([id, provider]) => {
|
|
// Check if tab doesn't exist
|
|
if (!document.querySelector(`.tab[data-provider="${id}"]`)) {
|
|
const button = document.createElement('button');
|
|
button.className = 'tab';
|
|
button.setAttribute('data-tab-type', 'anime');
|
|
button.setAttribute('data-provider', id);
|
|
button.innerHTML = `${provider.icon} ${provider.name}`;
|
|
button.onclick = () => switchTab(`anime-${id}`);
|
|
animeTabsContainer.appendChild(button);
|
|
|
|
// Create corresponding tab content
|
|
const tabContent = document.createElement('div');
|
|
tabContent.id = `tab-anime-${id}`;
|
|
tabContent.className = 'tab-content';
|
|
tabContent.innerHTML = createAnimeTabContent(id, provider);
|
|
document.querySelector('.container').insertBefore(
|
|
tabContent,
|
|
document.getElementById('downloadsList')
|
|
);
|
|
}
|
|
});
|
|
|
|
// Update supported hosts badges
|
|
const hostsContainer = document.querySelector('.supported-hosts');
|
|
hostsContainer.innerHTML = '';
|
|
|
|
Object.values(data.file_hosts).forEach(host => {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'host-badge';
|
|
badge.textContent = `${host.icon} ${host.name}`;
|
|
hostsContainer.appendChild(badge);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error loading providers:', error);
|
|
}
|
|
}
|
|
|
|
function createAnimeTabContent(providerId, provider) {
|
|
return `
|
|
<div class="url-form">
|
|
<div class="anime-input-group">
|
|
<input
|
|
type="text"
|
|
id="${providerId}UrlInput"
|
|
placeholder="URL de l'anime (ex: ${provider.url_pattern})"
|
|
>
|
|
<button type="button" class="btn-primary" onclick="loadProviderEpisodes('${providerId}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
|
</svg>
|
|
Charger
|
|
</button>
|
|
</div>
|
|
|
|
<div id="${providerId}EpisodeSelector" class="anime-select-group" style="display:none;">
|
|
<select id="${providerId}EpisodeSelect">
|
|
<option value="">Sélectionner un épisode</option>
|
|
</select>
|
|
<button type="button" class="btn-primary" onclick="downloadProviderEpisode('${providerId}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
Télécharger
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function loadProviderEpisodes(providerId) {
|
|
const animeUrl = document.getElementById(`${providerId}UrlInput`).value;
|
|
if (!animeUrl) {
|
|
alert('Veuillez entrer une URL d\'anime');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(animeUrl)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.episodes && data.episodes.length > 0) {
|
|
const select = document.getElementById(`${providerId}EpisodeSelect`);
|
|
select.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
|
|
|
data.episodes.forEach(ep => {
|
|
const option = document.createElement('option');
|
|
option.value = ep.url;
|
|
option.textContent = `Épisode ${ep.episode}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
document.getElementById(`${providerId}EpisodeSelector`).style.display = 'flex';
|
|
} else {
|
|
alert('Aucun épisode trouvé');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Erreur lors du chargement des épisodes');
|
|
}
|
|
}
|
|
|
|
async function downloadProviderEpisode(providerId) {
|
|
const episodeUrl = document.getElementById(`${providerId}EpisodeSelect`).value;
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById(`${providerId}EpisodeSelect`).value = '';
|
|
loadDownloads();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
// Tab switching
|
|
function switchTab(tabName) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
// Show selected tab
|
|
document.getElementById(`tab-${tabName}`).classList.add('active');
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
document.getElementById('downloadForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const url = document.getElementById('urlInput').value;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/download`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url })
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById('urlInput').value = '';
|
|
loadDownloads();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
});
|
|
|
|
// Anime functions
|
|
async function loadEpisodes() {
|
|
const animeUrl = document.getElementById('animeUrlInput').value;
|
|
if (!animeUrl) {
|
|
alert('Veuillez entrer une URL d\'anime');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(animeUrl)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.episodes && data.episodes.length > 0) {
|
|
const select = document.getElementById('episodeSelect');
|
|
select.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
|
|
|
data.episodes.forEach(ep => {
|
|
const option = document.createElement('option');
|
|
option.value = ep.url;
|
|
option.textContent = `Épisode ${ep.episode}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
document.getElementById('episodeSelector').style.display = 'flex';
|
|
currentAnimeUrl = animeUrl;
|
|
} else {
|
|
alert('Aucun épisode trouvé');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Erreur lors du chargement des épisodes');
|
|
}
|
|
}
|
|
|
|
async function downloadSelectedEpisode() {
|
|
const episodeUrl = document.getElementById('episodeSelect').value;
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById('episodeSelect').value = '';
|
|
loadDownloads();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
// Anime-Ultime functions
|
|
async function loadAnimeUltimeEpisodes() {
|
|
const animeUrl = document.getElementById('animeultimeUrlInput').value;
|
|
if (!animeUrl) {
|
|
alert('Veuillez entrer une URL d\'anime');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/episodes?url=${encodeURIComponent(animeUrl)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.episodes && data.episodes.length > 0) {
|
|
const select = document.getElementById('animeultimeEpisodeSelect');
|
|
select.innerHTML = '<option value="">Sélectionner un épisode</option>';
|
|
|
|
data.episodes.forEach(ep => {
|
|
const option = document.createElement('option');
|
|
option.value = ep.url;
|
|
option.textContent = `Épisode ${ep.episode}`;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
document.getElementById('animeultimeEpisodeSelector').style.display = 'flex';
|
|
} else {
|
|
alert('Aucun épisode trouvé');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
alert('Erreur lors du chargement des épisodes');
|
|
}
|
|
}
|
|
|
|
async function downloadAnimeUltimeEpisode() {
|
|
const episodeUrl = document.getElementById('animeultimeEpisodeSelect').value;
|
|
if (!episodeUrl) {
|
|
alert('Veuillez sélectionner un épisode');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/anime/download?url=${encodeURIComponent(episodeUrl)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById('animeultimeEpisodeSelect').value = '';
|
|
loadDownloads();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
}
|
|
}
|
|
|
|
async function loadDownloads() {
|
|
const response = await fetch(`${API_BASE}/downloads`);
|
|
const data = await response.json();
|
|
allDownloads = data.downloads; // Store all downloads
|
|
updateStats(); // Update statistics
|
|
filterDownloads(); // Apply current filters
|
|
}
|
|
|
|
function updateStats() {
|
|
const stats = {
|
|
total: allDownloads.length,
|
|
downloading: allDownloads.filter(d => d.status === 'downloading').length,
|
|
paused: allDownloads.filter(d => d.status === 'paused').length,
|
|
completed: allDownloads.filter(d => d.status === 'completed').length,
|
|
failed: allDownloads.filter(d => d.status === 'failed').length
|
|
};
|
|
|
|
document.getElementById('downloadsStats').innerHTML = `
|
|
<div class="stat-item">Total: <span class="stat-count">${stats.total}</span></div>
|
|
${stats.downloading > 0 ? `<div class="stat-item">En cours: <span class="stat-count" style="color: #00d9ff;">${stats.downloading}</span></div>` : ''}
|
|
${stats.paused > 0 ? `<div class="stat-item">En pause: <span class="stat-count" style="color: #ffa500;">${stats.paused}</span></div>` : ''}
|
|
${stats.completed > 0 ? `<div class="stat-item">Terminés: <span class="stat-count" style="color: #00ff88;">${stats.completed}</span></div>` : ''}
|
|
${stats.failed > 0 ? `<div class="stat-item">Échoués: <span class="stat-count" style="color: #ff4444;">${stats.failed}</span></div>` : ''}
|
|
`;
|
|
}
|
|
|
|
function displayDownloads(downloads, groupBy = 'none') {
|
|
const container = document.getElementById('downloadsList');
|
|
|
|
if (downloads.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
|
|
</svg>
|
|
<p>Aucun téléchargement trouvé</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Group downloads if needed
|
|
if (groupBy && groupBy !== 'none') {
|
|
const groups = groupDownloads(downloads, groupBy);
|
|
const groupNames = Object.keys(groups);
|
|
|
|
// Sort group names
|
|
groupNames.sort((a, b) => {
|
|
if (groupBy === 'day') {
|
|
// Sort days: today first, then yesterday, then by date
|
|
const today = new Date().toDateString();
|
|
const yesterday = new Date(Date.now() - 86400000).toDateString();
|
|
// Get actual dates from downloads to compare
|
|
return a.localeCompare(b);
|
|
}
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
// Display grouped downloads
|
|
let html = '';
|
|
groupNames.forEach((groupName, index) => {
|
|
const groupDownloads = groups[groupName];
|
|
const groupId = `group-${index}`;
|
|
const isCollapsed = collapsedGroups.has(groupId);
|
|
const collapsedClass = isCollapsed ? 'collapsed' : '';
|
|
const displayStyle = isCollapsed ? 'display: none;' : '';
|
|
|
|
html += `
|
|
<div class="downloads-group">
|
|
<div class="downloads-group-header ${collapsedClass}" onclick="toggleGroup('${groupId}')">
|
|
<div class="downloads-group-title">${escapeHtml(groupName)}</div>
|
|
<div class="downloads-group-count">${groupDownloads.length}</div>
|
|
</div>
|
|
<div class="downloads-group-items" id="${groupId}" style="${displayStyle}">
|
|
${groupDownloads.map(dl => renderDownloadItem(dl)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
container.innerHTML = html;
|
|
} else {
|
|
// Display flat list
|
|
container.innerHTML = downloads.map(dl => renderDownloadItem(dl)).join('');
|
|
}
|
|
}
|
|
|
|
function renderDownloadItem(dl) {
|
|
return `
|
|
<div class="download-item">
|
|
<div class="download-header">
|
|
<div class="filename">${escapeHtml(dl.filename)}</div>
|
|
<span class="status status-${dl.status}">${translateStatus(dl.status)}</span>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${dl.progress}%"></div>
|
|
</div>
|
|
<div class="download-info">
|
|
<span>${formatBytes(dl.downloaded_bytes)}${dl.total_bytes ? ' / ' + formatBytes(dl.total_bytes) : ''}</span>
|
|
<span>${dl.speed > 0 ? formatSpeed(dl.speed) : ''}</span>
|
|
</div>
|
|
<div class="download-actions">
|
|
${dl.status === 'downloading' ? `
|
|
<button class="btn-small btn-pause" onclick="pauseDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
Pause
|
|
</button>
|
|
<button class="btn-small btn-cancel" onclick="cancelDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
Annuler
|
|
</button>
|
|
` : ''}
|
|
${dl.status === 'paused' ? `
|
|
<button class="btn-small btn-resume" onclick="resumeDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
Reprendre
|
|
</button>
|
|
<button class="btn-small btn-cancel" onclick="cancelDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
Annuler
|
|
</button>
|
|
` : ''}
|
|
${dl.status === 'completed' ? `
|
|
<button class="btn-small btn-download" style="background: linear-gradient(45deg, #ff6b6b, #ffa500);" onclick="watchVideo('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
Regarder
|
|
</button>
|
|
<button class="btn-small btn-download" onclick="downloadFile('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
|
</svg>
|
|
Télécharger
|
|
</button>
|
|
<button class="btn-small btn-cancel" onclick="cancelDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
Supprimer
|
|
</button>
|
|
` : ''}
|
|
${dl.status === 'failed' ? `
|
|
<button class="btn-small btn-cancel" onclick="cancelDownload('${dl.id}')">
|
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
</svg>
|
|
Supprimer
|
|
</button>
|
|
` : ''}
|
|
</div>
|
|
${dl.error ? `<div class="error-message">${escapeHtml(dl.error)}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function toggleGroup(groupId) {
|
|
console.log('toggleGroup called with ID:', groupId);
|
|
const items = document.getElementById(groupId);
|
|
const header = items.previousElementSibling;
|
|
|
|
if (!items || !header) {
|
|
console.error('Could not find group elements');
|
|
return;
|
|
}
|
|
|
|
const isCollapsed = collapsedGroups.has(groupId);
|
|
console.log('isCollapsed:', isCollapsed);
|
|
|
|
if (isCollapsed) {
|
|
// Expand
|
|
items.style.display = 'flex';
|
|
header.classList.remove('collapsed');
|
|
collapsedGroups.delete(groupId);
|
|
console.log('Expanded group');
|
|
} else {
|
|
// Collapse
|
|
items.style.display = 'none';
|
|
header.classList.add('collapsed');
|
|
collapsedGroups.add(groupId);
|
|
console.log('Collapsed group');
|
|
}
|
|
}
|
|
|
|
async function pauseDownload(id) {
|
|
await fetch(`${API_BASE}/download/${id}/pause`, { method: 'POST' });
|
|
loadDownloads();
|
|
}
|
|
|
|
async function resumeDownload(id) {
|
|
await fetch(`${API_BASE}/download/${id}/resume`, { method: 'POST' });
|
|
loadDownloads();
|
|
}
|
|
|
|
async function cancelDownload(id) {
|
|
if (confirm('Êtes-vous sûr ?')) {
|
|
await fetch(`${API_BASE}/download/${id}`, { method: 'DELETE' });
|
|
loadDownloads();
|
|
}
|
|
}
|
|
|
|
function downloadFile(id) {
|
|
window.open(`${API_BASE}/download/${id}/file`, '_blank');
|
|
}
|
|
|
|
function watchVideo(id) {
|
|
window.open(`/player/${id}`, '_blank');
|
|
}
|
|
|
|
function translateStatus(status) {
|
|
const translations = {
|
|
'pending': 'En attente',
|
|
'downloading': 'Téléchargement',
|
|
'paused': 'En pause',
|
|
'completed': 'Terminé',
|
|
'failed': 'Échoué',
|
|
'cancelled': 'Annulé'
|
|
};
|
|
return translations[status] || status;
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatSpeed(bytesPerSecond) {
|
|
return formatBytes(bytesPerSecond) + '/s';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Initialize: Load providers and start downloads refresh
|
|
loadProviders();
|
|
loadDownloads();
|
|
setInterval(loadDownloads, 1000);
|
|
</script>
|
|
</body>
|
|
</html>
|