f527a335de
Change from using 'this' pointer to using unique IDs for group toggling: - Generate unique IDs for each group (group-0, group-1, etc.) - Pass group ID to toggleGroup() function instead of DOM element - Use getElementById() and previousElementSibling to find elements - Add error handling with console.error for missing elements - Add console.log statements for debugging This approach is more reliable than relying on inline onclick handlers with 'this' keyword, especially when the HTML is dynamically generated.
1736 lines
65 KiB
HTML
1736 lines
65 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 les 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
|
|
|
|
// 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();
|
|
|
|
// 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 completed downloads
|
|
async function clearCompleted() {
|
|
const completed = allDownloads.filter(dl => dl.status === 'completed');
|
|
|
|
if (completed.length === 0) {
|
|
alert('Aucun téléchargement terminé à supprimer');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`Supprimer ${completed.length} téléchargement(s) terminé(s) ?`)) {
|
|
return;
|
|
}
|
|
|
|
for (const dl of completed) {
|
|
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}`;
|
|
html += `
|
|
<div class="downloads-group">
|
|
<div class="downloads-group-header" 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}">
|
|
${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 = header.classList.contains('collapsed');
|
|
console.log('isCollapsed:', isCollapsed);
|
|
|
|
if (isCollapsed) {
|
|
// Expand
|
|
items.style.display = 'flex';
|
|
header.classList.remove('collapsed');
|
|
console.log('Expanded group');
|
|
} else {
|
|
// Collapse
|
|
items.style.display = 'none';
|
|
header.classList.add('collapsed');
|
|
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>
|