Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user