376 lines
14 KiB
PHP
376 lines
14 KiB
PHP
<?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;
|
||
}
|
||
}
|