feat(demandes): telechargement auto lors de l'aprobation

feat(demandes): ajout des demandes par épisodes
This commit is contained in:
MasutaYunikon committed 2026-05-22 20:43:30 +02:00
1 parent f3d535d577
commit 473d97e392
5 files changed
+187 -36

No files matched your search

+10 -3
View File
@@ -206,12 +206,13 @@
<button
v-if="requestMode"
class="w-8 h-8 rounded-lg border flex items-center justify-center transition-colors"
:class="(requestedSeasons?.includes(season.season_number))
:class="isEpisodeRequested(ep.id)
? 'border-blue-500/30 text-blue-400 bg-blue-500/10 cursor-default'
: 'border-border text-muted hover:border-accent hover:text-accent cursor-pointer'"
@click="!requestedSeasons?.includes(season.season_number) && emit('requestEpisode', season.season_number, ep.id)"
:title="isEpisodeRequested(ep.id) ? 'Déjà demandé' : 'Demander cet épisode'"
@click="!isEpisodeRequested(ep.id) && emit('requestEpisode', season.season_number, ep.id)"
>
<component :is="requestedSeasons?.includes(season.season_number) ? checkIcon : requestIcon" />
<component :is="isEpisodeRequested(ep.id) ? checkIcon : requestIcon" />
</button>
<!-- Mode téléchargement (admin) -->
<button
@@ -347,6 +348,7 @@ const props = defineProps<{
nfoSupport ?: boolean
requestMode ?: boolean // true = boutons demande au lieu de téléchargement
requestedSeasons ?: number[] // saisons déjà demandées par cet user pour cette série
requestedEpisodes ?: number[] // IDs d'épisodes déjà demandés par cet user pour cette série
}>()
const emit = defineEmits<{
@@ -369,6 +371,11 @@ const isSeasonRequested = computed(() =>
props.requestedSeasons?.includes(props.season.season_number) ?? false
)
/** Un épisode est "demandé" si sa saison l'est, ou si son ID est dans requestedEpisodes */
function isEpisodeRequested(episodeId: number): boolean {
return isSeasonRequested.value || (props.requestedEpisodes?.includes(episodeId) ?? false)
}
const epOptionsOpen = ref<number | null>(null)
const plotOpen = ref<number | null>(null)
const seasonMenuOpen = ref(false)
+18 -7
View File
@@ -70,12 +70,14 @@
{{ statusLabel(req.status) }}
</span>
</div>
<!-- Saisons fusionnées -->
<!-- Saisons / épisodes fusionnés -->
<p class="text-xs text-muted mt-1">
<template v-if="mergedSeasons(req).length === 0">Toutes les saisons</template>
<template v-else>
Saisons : {{ mergedSeasons(req).map(s => s === 0 ? 'SP' : `S${s}`).join(', ') }}
<template v-if="mergedEpisodes(req).length > 0">
{{ mergedEpisodes(req).length }} épisode{{ mergedEpisodes(req).length > 1 ? 's' : '' }} spécifique{{ mergedEpisodes(req).length > 1 ? 's' : '' }}
<template v-if="mergedSeasons(req).length > 0"> · Saisons : {{ mergedSeasons(req).map((s: number) => s === 0 ? 'SP' : `S${s}`).join(', ') }}</template>
</template>
<template v-else-if="mergedSeasons(req).length === 0">Toutes les saisons</template>
<template v-else>Saisons : {{ mergedSeasons(req).map((s: number) => s === 0 ? 'SP' : `S${s}`).join(', ') }}</template>
</p>
<!-- Message de refus -->
<p v-if="req.rejectionMessage" class="text-xs text-red-400 mt-1">
@@ -112,7 +114,10 @@
{{ r.username[0].toUpperCase() }}
</span>
<span class="text-muted">{{ r.username }}</span>
<span v-if="r.seasons.length > 0" class="text-muted/60">
<span v-if="r.episodes?.length > 0" class="text-muted/60">
({{ r.episodes.length }} ep.)
</span>
<span v-else-if="r.seasons.length > 0" class="text-muted/60">
({{ r.seasons.map((s: number) => s === 0 ? 'SP' : `S${s}`).join(', ') }})
</span>
</div>
@@ -149,7 +154,7 @@
import { ref, computed, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
interface Requester { userId: string; username: string; seasons: number[]; requestedAt: string }
interface Requester { userId: string; username: string; seasons: number[]; episodes: number[]; requestedAt: string }
interface SerieRequest {
id: string; serieId: number; serieName: string
requesters: Requester[]; status: string
@@ -230,12 +235,18 @@ async function confirmReject() {
// ── Helpers ───────────────────────────────────────────────────
function mergedSeasons(req: SerieRequest): number[] {
if (req.requesters.some(r => r.seasons.length === 0)) return []
if (req.requesters.some(r => r.seasons.length === 0 && (r.episodes?.length ?? 0) === 0)) return []
const all = new Set<number>()
req.requesters.forEach(r => r.seasons.forEach(s => all.add(s)))
return [...all].sort((a, b) => a - b)
}
function mergedEpisodes(req: SerieRequest): number[] {
const all = new Set<number>()
req.requesters.forEach(r => (r.episodes ?? []).forEach(e => all.add(e)))
return [...all].sort((a, b) => a - b)
}
function statusLabel(s: string) {
return { pending: 'En attente', approved: 'Approuvée', rejected: 'Refusée', completed: 'Disponible' }[s] ?? s
}
+37 -6
View File
@@ -139,6 +139,7 @@
:nfo-support="nfoSupport"
:request-mode="!auth.isAdmin"
:requested-seasons="requestedSeasonNumbers"
:requested-episodes="requestedEpisodeIds"
@toggle="toggleSeason"
@download="(key, url, magnet, fi, fp, ih) => download(key, url, magnet, fi, fp, ih)"
@download-season="(s, h) => downloadSeason(s, h)"
@@ -248,13 +249,19 @@ const requestSeasons = ref<number[]>([]) // saisons cochées dans le modal
const requestedSeasonNumbers = computed<number[]>(() => {
if (!myRequest.value) return []
// Trouver l'entrée de l'utilisateur connecté dans la demande fusionnée
const r = myRequest.value.requesters?.find((r: any) => r.userId === auth.userId)
// [] = toutes les saisons demandées → on retourne toutes les saisons de la série
if (r && r.seasons.length === 0) return data.value?.seasons?.map((s: any) => s.season_number) ?? []
return r?.seasons ?? []
})
const requestedEpisodeIds = computed<number[]>(() => {
if (!myRequest.value) return []
const r = myRequest.value.requesters?.find((r: any) => r.userId === auth.userId)
if (!r) return []
return r.episodes ?? []
})
async function fetchMyRequest() {
try {
const res = await fetch('/api/requests', { credentials: 'include' })
@@ -282,8 +289,28 @@ function handleRequestSeason(seasonNumber: number) {
openRequestModal(seasonNumber)
}
function handleRequestEpisode(seasonNumber: number, _episodeId: number) {
openRequestModal(seasonNumber)
async function handleRequestEpisode(_seasonNumber: number, episodeId: number) {
if (!data.value) return
try {
const res = await fetch('/api/requests', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
serieId : Number(route.params.id),
serieName: data.value.serie.title,
seasons : [],
episodes : [episodeId],
}),
})
if (res.ok) {
myRequest.value = await res.json()
toast('Épisode demandé ✓', 'success')
} else {
const d = await res.json()
toast(d.error ?? 'Erreur', 'error')
}
} catch { toast('Impossible de contacter le serveur', 'error') }
}
async function submitRequest() {
@@ -701,9 +728,13 @@ async function unimportEpisode(ep: any, _season: any, deleteFile: boolean) {
const closeMenus = () => { downloadMenuOpen.value = false }
onMounted(() => {
load(); fetchSettings(); fetchOrganized(); fetchActiveDownloads(); fetchRssSync()
if (!auth.isAdmin) fetchMyRequest()
pollTimer = setInterval(fetchActiveDownloads, 5000)
load()
if (auth.isAdmin) {
fetchSettings(); fetchOrganized(); fetchActiveDownloads(); fetchRssSync()
pollTimer = setInterval(fetchActiveDownloads, 5000)
} else {
fetchMyRequest()
}
document.addEventListener('click', closeMenus)
})
onUnmounted(() => {