Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'AI — Toplu Anime Meta Doldurma')
|
||||
@section('page-title', 'AI — Toplu Anime Meta Doldurma')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
|
||||
{{-- İstatistikler --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-info">{{ $total }}</div>
|
||||
<div class="text-muted small">Toplam anime</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-warning">{{ $missing }}</div>
|
||||
<div class="text-muted small">Eksik meta alanı olan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-danger">{{ $noGenres }}</div>
|
||||
<div class="text-muted small">Kategori ataması yok</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<h6 class="mb-1">Toplu Anime Meta Doldurma</h6>
|
||||
<p class="text-muted small mb-3">
|
||||
DeepSeek AI, eksik alanı olan animeleri tek tek işler. Açıklama, yıl, stüdyo, tür, durum, puan ve kategorileri doldurur.
|
||||
Dolu alanların üstüne yazmaz. Anime başına ~3-5 sn sürer.
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mb-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="force-mode">
|
||||
<label class="form-check-label small" for="force-mode">Dolu alanları da yenile (force)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button id="start-btn" class="btn btn-info px-4" onclick="startFill()">
|
||||
<i class="bi bi-stars"></i> Başlat
|
||||
</button>
|
||||
<button id="stop-btn" class="btn btn-outline-danger px-4" onclick="stopFill()" style="display:none">
|
||||
<i class="bi bi-stop-fill"></i> Durdur
|
||||
</button>
|
||||
<button id="reset-btn" class="btn btn-outline-secondary px-4" onclick="resetFill()" style="display:none">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Sıfırla
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- İlerleme --}}
|
||||
<div id="progress-wrap" style="display:none">
|
||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||
<span id="prog-label">Hazırlanıyor…</span>
|
||||
<span id="prog-count">0 / 0</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height:10px;border-radius:6px">
|
||||
<div id="prog-bar" class="progress-bar bg-info progress-bar-striped progress-bar-animated" style="width:0%"></div>
|
||||
</div>
|
||||
<div class="d-flex gap-3 small text-muted mb-3">
|
||||
<span>✅ <span id="cnt-ok">0</span> tamamlandı</span>
|
||||
<span>⏭ <span id="cnt-skip">0</span> atlandı</span>
|
||||
<span>❌ <span id="cnt-err">0</span> hata</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Log --}}
|
||||
<div id="log-box" style="display:none;background:#0d0d0d;border:1px solid #222;border-radius:8px;padding:12px;max-height:420px;overflow-y:auto;font-family:monospace;font-size:.78rem;line-height:1.6"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let _animes = [], _idx = 0, _stop = false;
|
||||
let _ok = 0, _skip = 0, _err = 0;
|
||||
|
||||
const logBox = document.getElementById('log-box');
|
||||
const progWrap = document.getElementById('progress-wrap');
|
||||
const progBar = document.getElementById('prog-bar');
|
||||
const progLbl = document.getElementById('prog-label');
|
||||
const progCnt = document.getElementById('prog-count');
|
||||
|
||||
function log(html) {
|
||||
logBox.insertAdjacentHTML('beforeend', `<div>${html}</div>`);
|
||||
logBox.scrollTop = logBox.scrollHeight;
|
||||
}
|
||||
|
||||
async function startFill() {
|
||||
document.getElementById('start-btn').style.display = 'none';
|
||||
document.getElementById('stop-btn').style.display = '';
|
||||
document.getElementById('reset-btn').style.display = 'none';
|
||||
progWrap.style.display = '';
|
||||
logBox.style.display = '';
|
||||
logBox.innerHTML = '';
|
||||
_stop = false; _ok = 0; _skip = 0; _err = 0;
|
||||
updateCounts();
|
||||
|
||||
const force = document.getElementById('force-mode').checked;
|
||||
|
||||
log('<span style="color:#00c8d4">⏳ Anime listesi alınıyor…</span>');
|
||||
|
||||
const res = await fetch('{{ route("ai.anime-meta-ids") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ force }),
|
||||
});
|
||||
const data = await res.json();
|
||||
_animes = data.animes || [];
|
||||
_idx = 0;
|
||||
|
||||
if (!_animes.length) {
|
||||
log('<span style="color:#4caf50">✅ Tüm animeler zaten dolu, yapılacak bir şey yok.</span>');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
log(`<span style="color:#ccc">📋 ${_animes.length} anime işlenecek.</span>`);
|
||||
progBar.style.width = '0%';
|
||||
|
||||
await processNext(force);
|
||||
}
|
||||
|
||||
async function processNext(force) {
|
||||
if (_stop || _idx >= _animes.length) { finish(); return; }
|
||||
|
||||
const anime = _animes[_idx];
|
||||
_idx++;
|
||||
|
||||
const pct = Math.round((_idx / _animes.length) * 100);
|
||||
progBar.style.width = pct + '%';
|
||||
progCnt.textContent = `${_idx} / ${_animes.length}`;
|
||||
progLbl.textContent = anime.title;
|
||||
|
||||
const missingStr = anime.missing.length ? anime.missing.join(', ') : '—';
|
||||
log(`<span style="color:#888">[${_idx}/${_animes.length}]</span> <span style="color:#e8e8e8">${anime.title}</span> <span style="color:#555">— eksik: ${missingStr}</span>`);
|
||||
|
||||
try {
|
||||
const r = await fetch('{{ route("ai.fill-anime-meta") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ anime_id: anime.id, force }),
|
||||
});
|
||||
const d = await r.json();
|
||||
|
||||
if (d.ok) {
|
||||
const fields = (d.filled || []).join(', ') || 'değişiklik yok';
|
||||
log(` <span style="color:#4caf50">✅ OK</span> → ${fields}`);
|
||||
_ok++;
|
||||
} else {
|
||||
log(` <span style="color:#ff5722">❌ HATA</span> → ${d.error || 'Bilinmeyen hata'}`);
|
||||
_err++;
|
||||
}
|
||||
} catch (e) {
|
||||
log(` <span style="color:#ff5722">❌ HATA</span> → ${e.message}`);
|
||||
_err++;
|
||||
}
|
||||
|
||||
updateCounts();
|
||||
|
||||
// 1.5sn bekle, sonraki anime'ye geç
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
await processNext(force);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
progBar.classList.remove('progress-bar-animated');
|
||||
progBar.style.width = '100%';
|
||||
progLbl.textContent = 'Tamamlandı';
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('reset-btn').style.display = '';
|
||||
document.getElementById('start-btn').style.display = '';
|
||||
log(`<br><span style="color:#00c8d4;font-weight:700">🏁 Bitti → ${_ok} tamamlandı, ${_skip} atlandı, ${_err} hata</span>`);
|
||||
}
|
||||
|
||||
function stopFill() {
|
||||
_stop = true;
|
||||
log('<span style="color:#ff9800">⏸ Kullanıcı tarafından durduruldu.</span>');
|
||||
}
|
||||
|
||||
function resetFill() {
|
||||
_animes = []; _idx = 0; _ok = 0; _skip = 0; _err = 0;
|
||||
logBox.innerHTML = '';
|
||||
logBox.style.display = 'none';
|
||||
progWrap.style.display = 'none';
|
||||
progBar.style.width = '0%';
|
||||
progBar.classList.add('progress-bar-animated');
|
||||
document.getElementById('reset-btn').style.display = 'none';
|
||||
document.getElementById('start-btn').style.display = '';
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
document.getElementById('cnt-ok').textContent = _ok;
|
||||
document.getElementById('cnt-skip').textContent = _skip;
|
||||
document.getElementById('cnt-err').textContent = _err;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,141 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'AI Açıklama Yazma')
|
||||
@section('page-title', 'AI — Toplu Bölüm Açıklaması')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-7 col-lg-8">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-stars" style="color:var(--info)"></i>
|
||||
<h6>Toplu Açıklama Yaz</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:20px">
|
||||
DeepSeek, seçilen animenin açıklaması boş olan tüm bölümlerine sırayla Türkçe açıklama yazar.
|
||||
Bölüm başına ~2-3 saniye sürer.
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Anime Seç</label>
|
||||
<select id="anime-select" class="form-select" style="max-width:480px">
|
||||
<option value="0">— Tüm animeler ({{ $totalMissing }} boş bölüm) —</option>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}">{{ $anime->title }} ({{ $anime->total_eps }} boş bölüm)</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="start-btn" class="btn btn-info px-4" onclick="startFill()">
|
||||
<i class="bi bi-stars"></i> Başlat
|
||||
</button>
|
||||
<button id="stop-btn" class="btn btn-outline-danger px-4" onclick="stopFill()" style="display:none">
|
||||
<i class="bi bi-stop-fill"></i> Durdur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Progress --}}
|
||||
<div id="progress-card" class="card mb-4" style="display:none">
|
||||
<div class="card-header">
|
||||
<h6>İlerleme</h6>
|
||||
<span id="prog-count" style="font-size:12.5px;color:var(--text2);margin-left:auto">0 / 0</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="background:var(--surface);border-radius:99px;height:8px;overflow:hidden;margin-bottom:14px">
|
||||
<div id="prog-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--info),var(--accent-lt));border-radius:99px;transition:width .3s"></div>
|
||||
</div>
|
||||
<div id="prog-log" style="max-height:360px;overflow-y:auto;font-size:12px;font-family:'JetBrains Mono',monospace;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;line-height:1.7"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Result --}}
|
||||
<div id="result-card" class="card" style="display:none">
|
||||
<div class="card-body">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text);margin-bottom:6px">Tamamlandı</div>
|
||||
<div id="result-text" style="font-size:13.5px;color:var(--text2)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = '{{ csrf_token() }}';
|
||||
const URL_IDS = '{{ route("admin.ai.episode-ids") }}';
|
||||
const URL_FILL = '{{ route("admin.ai.fill-one") }}';
|
||||
|
||||
let running = false, shouldStop = false;
|
||||
|
||||
async function startFill() {
|
||||
const animeId = document.getElementById('anime-select').value;
|
||||
running = true; shouldStop = false;
|
||||
document.getElementById('start-btn').disabled = true;
|
||||
document.getElementById('stop-btn').style.display = 'inline-flex';
|
||||
document.getElementById('progress-card').style.display = 'block';
|
||||
document.getElementById('result-card').style.display = 'none';
|
||||
document.getElementById('prog-log').innerHTML = '';
|
||||
document.getElementById('prog-bar').style.width = '0%';
|
||||
|
||||
log('📋 Bölüm listesi alınıyor...', 'info');
|
||||
let episodes;
|
||||
try {
|
||||
const res = await post(URL_IDS, { anime_id: animeId });
|
||||
episodes = res.episodes || [];
|
||||
} catch(e) { log('❌ ' + e.message, 'danger'); finish(0,0); return; }
|
||||
|
||||
if (!episodes.length) { log('✅ Tüm açıklamalar zaten dolu!', 'success'); finish(0,0); return; }
|
||||
log(`🎯 ${episodes.length} boş açıklama. Başlıyor...`, 'info');
|
||||
document.getElementById('prog-count').textContent = '0 / ' + episodes.length;
|
||||
|
||||
let done = 0, failed = 0;
|
||||
for (let i = 0; i < episodes.length; i++) {
|
||||
if (shouldStop) { log('⏹ Durduruldu.', 'warning'); break; }
|
||||
const ep = episodes[i];
|
||||
log(`⏳ [${i+1}/${episodes.length}] ${ep.label}`, 'muted');
|
||||
try {
|
||||
const res = await post(URL_FILL, { episode_id: ep.id });
|
||||
if (res.ok) { done++; log(`✓ [${i+1}/${episodes.length}] ${ep.label}`, 'success'); }
|
||||
else { failed++; log(`✗ [${i+1}/${episodes.length}] ${ep.label} — ${res.error||'Hata'}`, 'danger'); }
|
||||
} catch(e) { failed++; log(`✗ ${ep.label} — ${e.message}`, 'danger'); }
|
||||
const pct = Math.round(((i+1)/episodes.length)*100);
|
||||
document.getElementById('prog-bar').style.width = pct+'%';
|
||||
document.getElementById('prog-count').textContent = (i+1)+' / '+episodes.length;
|
||||
document.getElementById('prog-log').scrollTop = document.getElementById('prog-log').scrollHeight;
|
||||
}
|
||||
finish(done, failed);
|
||||
}
|
||||
|
||||
function stopFill() { shouldStop = true; }
|
||||
|
||||
function finish(done, failed) {
|
||||
running = false;
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
const res = document.getElementById('result-card');
|
||||
res.style.display = 'block';
|
||||
document.getElementById('result-text').innerHTML =
|
||||
`<span style="color:var(--success);font-weight:700">${done} bölüm</span> yazıldı` +
|
||||
(failed ? `, <span style="color:var(--danger);font-weight:700">${failed} hata</span>` : '') + '.';
|
||||
}
|
||||
|
||||
function log(msg, type='muted') {
|
||||
const c = { success:'var(--success)', danger:'var(--danger)', info:'var(--info)', warning:'var(--warning)', muted:'var(--text3)' };
|
||||
const el = document.getElementById('prog-log');
|
||||
el.innerHTML += `<div style="color:${c[type]||c.muted}">${msg}</div>`;
|
||||
}
|
||||
|
||||
async function post(url, body) {
|
||||
const r = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF}, body:JSON.stringify(body) });
|
||||
const data = await r.json();
|
||||
if (!r.ok && !data.ok) throw new Error(data.error||r.status);
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
Reference in New Issue
Block a user