Files
animexe/app/Http/Controllers/Admin/ImportController.php
T
2026-07-14 00:01:48 +03:00

376 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ImportJob;
use App\Models\Episode;
use App\Models\Season;
use App\Models\Subtitle;
use App\Models\VideoSource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ImportController extends Controller
{
public function index()
{
$jobs = ImportJob::latest()->paginate(20);
// Araçlar paneli için istatistikler
$stats = [
'total_animes' => \App\Models\Anime::where('is_published', true)->count(),
'anizium_done' => ImportJob::where('source', 'anizium')->where('status', 'done')->count(),
'animecix_done' => ImportJob::where('source', 'animecix')->where('status', 'done')->count(),
'video_sources_total' => VideoSource::count(),
'anizium_sources' => VideoSource::where('source', 'anizium')->count(),
'animecix_sources' => VideoSource::where('source', 'animecix')->count(),
'subtitle_mismatch' => $this->countSubtitleMismatch(),
];
return view('admin.import.index', compact('jobs', 'stats'));
}
public function store(Request $request)
{
$request->validate([
'source_url' => 'required|url|max:500',
'cdn_id' => 'nullable|string|max:50',
'anime_title' => 'nullable|string|max:255',
'season_ranges' => 'nullable|array',
'season_ranges.*.season' => 'required_with:season_ranges|integer|min:1',
'season_ranges.*.from' => 'required_with:season_ranges|integer|min:1',
'season_ranges.*.to' => 'required_with:season_ranges|integer|min:1',
]);
$watchId = null;
if ($request->source_url) {
preg_match('/\/(?:anime|watch)\/(\d+)/', $request->source_url, $m);
$watchId = $m[1] ?? null;
}
$ranges = null;
if ($request->filled('season_ranges')) {
$ranges = [];
foreach ($request->season_ranges as $r) {
if (empty($r['season']) || empty($r['from']) || empty($r['to'])) continue;
$from = (int) $r['from'];
$to = (int) $r['to'];
if ($from > $to) [$from, $to] = [$to, $from];
$ranges[] = ['season' => (int)$r['season'], 'from' => $from, 'to' => $to];
}
if (empty($ranges)) $ranges = null;
}
$job = ImportJob::create([
'source_url' => $request->source_url,
'watch_id' => $watchId,
'cdn_id' => $request->cdn_id ? trim($request->cdn_id) : null,
'anime_title' => $request->anime_title,
'season_ranges' => $ranges,
'status' => 'pending',
]);
return redirect()->route('admin.import.show', $job)
->with('success', "Import job #{$job->id} oluşturuldu. Python script'i başlatın.");
}
public function show(ImportJob $import)
{
return view('admin.import.show', compact('import'));
}
public function destroy(ImportJob $import)
{
$import->delete();
return redirect()->route('admin.import.index')->with('success', 'Job silindi.');
}
public function destroyFailed()
{
$count = ImportJob::where('status', 'failed')->count();
ImportJob::where('status', 'failed')->delete();
return redirect()->route('admin.import.index')->with('success', "{$count} hatalı job silindi.");
}
public function destroyPending()
{
$count = ImportJob::where('status', 'pending')->count();
ImportJob::where('status', 'pending')->delete();
return redirect()->route('admin.import.index')->with('success', "{$count} bekleyen job silindi.");
}
public function destroyStuck()
{
// fetching/downloading/uploading ama 2 saatten fazladır güncellenmemiş = takılı kalmış
$cutoff = now()->subHours(2);
$count = ImportJob::whereIn('status', ['fetching', 'downloading', 'uploading'])
->where('updated_at', '<', $cutoff)
->count();
ImportJob::whereIn('status', ['fetching', 'downloading', 'uploading'])
->where('updated_at', '<', $cutoff)
->delete();
return redirect()->route('admin.import.index')->with('success', "{$count} takılı kalmış job silindi.");
}
public function bulkCounts()
{
$cutoff = now()->subHours(2);
return response()->json([
'failed' => ImportJob::where('status', 'failed')->count(),
'pending' => ImportJob::where('status', 'pending')->count(),
'stuck' => ImportJob::whereIn('status', ['fetching', 'downloading', 'uploading'])
->where('updated_at', '<', $cutoff)
->count(),
]);
}
public function destroyByStatus(Request $request)
{
$statuses = $request->input('statuses', []);
$hours = (int) $request->input('stuck_hours', 2);
$allowed = ['pending', 'failed', 'fetching', 'downloading', 'uploading'];
$statuses = array_intersect($statuses, $allowed);
if (empty($statuses)) {
return response()->json(['ok' => false, 'message' => 'Geçerli status seçilmedi.'], 422);
}
$query = ImportJob::whereIn('status', $statuses);
// Aktif statüler için sadece belirtilen saatten eskilerini sil
$activeStatuses = array_intersect($statuses, ['fetching', 'downloading', 'uploading']);
if (!empty($activeStatuses) && count($activeStatuses) === count($statuses)) {
$query->where('updated_at', '<', now()->subHours($hours));
}
$count = $query->count();
$query->delete();
return response()->json(['ok' => true, 'deleted' => $count]);
}
// ── ARAÇLAR: Terminal gerektirmez, admin panelden çalışır ─────────────────
/**
* Altyazı uyuşmazlığı düzelt (Anizium episode-1 cache bug).
* Subtitle URL'sindeki name=s1_b1_XX yanlış bölümü işaret edenleri siler.
*/
public function fixSubtitles(Request $request)
{
$dryRun = $request->boolean('dry_run', false);
$animeId = $request->input('anime_id');
$query = Subtitle::query()
->join('episodes', 'subtitles.episode_id', '=', 'episodes.id')
->join('seasons', 'seasons.id', '=', 'episodes.season_id')
->whereNotNull('subtitles.url')
->where('subtitles.url', 'like', '%anizium%')
->select(
'subtitles.id as subtitle_id',
'subtitles.language',
'subtitles.url',
'seasons.season_number',
'episodes.episode_number',
'episodes.anime_id',
);
if ($animeId) {
$query->where('episodes.anime_id', (int) $animeId);
}
$subtitles = $query->get();
$mismatchIds = [];
$details = [];
foreach ($subtitles as $sub) {
$parsed = parse_url($sub->url);
if (!isset($parsed['query'])) continue;
parse_str($parsed['query'], $params);
$name = $params['name'] ?? '';
if (!$name) continue;
$expectedPrefix = "s{$sub->season_number}_b{$sub->episode_number}_";
if (!str_starts_with($name, $expectedPrefix)) {
$mismatchIds[] = $sub->subtitle_id;
$details[] = [
'anime_id' => $sub->anime_id,
'season' => $sub->season_number,
'episode' => $sub->episode_number,
'lang' => $sub->language,
'name' => $name,
'expected' => $expectedPrefix . $sub->language,
];
}
}
$deleted = 0;
if (!$dryRun && !empty($mismatchIds)) {
$deleted = Subtitle::whereIn('id', $mismatchIds)->delete();
}
return response()->json([
'ok' => true,
'dry_run' => $dryRun,
'checked' => $subtitles->count(),
'mismatch' => count($mismatchIds),
'deleted' => $deleted,
'details' => array_slice($details, 0, 30),
]);
}
/**
* Anizium done job'larını yeniden pending yap (yeni video_sources eklemek için).
* Her job'ın done_episodes sıfırlanır; Anizium bot yeniden çalışınca
* doneEpisodes() artık source='anizium' kontrolü yaptığından
* sadece video_sources'ta anizium kaydı OLMAYAN bölümleri yeniden işler.
*/
public function requeueAnizium(Request $request)
{
$limit = (int) $request->input('limit', 50);
$animeId = $request->input('anime_id');
$query = ImportJob::where('source', 'anizium')
->where('status', 'done')
->whereNotNull('watch_id')
->latest();
if ($animeId) {
$query->where('anime_id', (int) $animeId);
}
$jobs = $query->limit($limit)->get();
$requeued = 0;
foreach ($jobs as $job) {
// Zaten pending/işleniyor olan var mı?
$active = ImportJob::where('watch_id', $job->watch_id)
->where('source', 'anizium')
->whereIn('status', ['pending', 'fetching', 'downloading', 'uploading'])
->exists();
if (!$active) {
$job->update([
'status' => 'pending',
'done_episodes'=> 0,
'error_log' => null,
'current_step' => 'Çapraz re-import — video_sources yenileme',
]);
$requeued++;
}
}
return response()->json([
'ok' => true,
'checked' => $jobs->count(),
'requeued' => $requeued,
]);
}
/**
* AnimeCix done job'larını yeniden pending yap.
*/
public function requeueAnimecix(Request $request)
{
$limit = (int) $request->input('limit', 50);
$animeId = $request->input('anime_id');
$query = ImportJob::where('source', 'animecix')
->where('status', 'done')
->whereNotNull('animecix_title_id')
->latest();
if ($animeId) {
$query->where('anime_id', (int) $animeId);
}
$jobs = $query->limit($limit)->get();
$requeued = 0;
foreach ($jobs as $job) {
$active = ImportJob::where('animecix_title_id', $job->animecix_title_id)
->where('source', 'animecix')
->whereIn('status', ['pending', 'fetching'])
->exists();
if (!$active) {
$job->update([
'status' => 'pending',
'done_episodes'=> 0,
'error_log' => null,
'current_step' => 'Çapraz re-import — video_sources yenileme',
]);
$requeued++;
}
}
return response()->json([
'ok' => true,
'checked' => $jobs->count(),
'requeued' => $requeued,
]);
}
/**
* video_sources istatistikleri (AJAX için).
*/
public function sourceStats()
{
$animeCount = \App\Models\Anime::where('is_published', true)->count();
$episodesWithBoth = DB::table('episodes')
->whereExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'anizium'))
->whereExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'animecix'))
->where('is_published', true)
->count();
$episodesOnlyAnizium = DB::table('episodes')
->whereExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'anizium'))
->whereNotExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'animecix'))
->where('is_published', true)
->count();
$episodesOnlyAnimecix = DB::table('episodes')
->whereExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'animecix'))
->whereNotExists(fn($q) => $q->from('video_sources')->whereColumn('video_sources.episode_id', 'episodes.id')->where('video_sources.source', 'anizium'))
->where('is_published', true)
->count();
return response()->json([
'anime_count' => $animeCount,
'episodes_with_both' => $episodesWithBoth,
'episodes_only_anizium' => $episodesOnlyAnizium,
'episodes_only_animecix' => $episodesOnlyAnimecix,
'subtitle_mismatch' => $this->countSubtitleMismatch(),
'anizium_pending_jobs' => ImportJob::where('source', 'anizium')->where('status', 'pending')->count(),
'animecix_pending_jobs' => ImportJob::where('source', 'animecix')->where('status', 'pending')->count(),
]);
}
// ── Yardımcı ─────────────────────────────────────────────────────────────
private function countSubtitleMismatch(): int
{
$rows = Subtitle::query()
->join('episodes', 'subtitles.episode_id', '=', 'episodes.id')
->join('seasons', 'seasons.id', '=', 'episodes.season_id')
->whereNotNull('subtitles.url')
->where('subtitles.url', 'like', '%anizium%')
->select('subtitles.url', 'seasons.season_number', 'episodes.episode_number')
->get();
$count = 0;
foreach ($rows as $r) {
$parsed = parse_url($r->url);
if (!isset($parsed['query'])) continue;
parse_str($parsed['query'], $params);
$name = $params['name'] ?? '';
if ($name && !str_starts_with($name, "s{$r->season_number}_b{$r->episode_number}_")) {
$count++;
}
}
return $count;
}
}