Files
animexe/routes/web.php
T
ayrisdevandClaude Opus 4.8 87e0d7dcf5 Perf: drop session/bot middleware from /media so Cloudflare can cache images
Media responses carried Laravel session cookies (Set-Cookie), which made
Cloudflare BYPASS the cache for every cover/avatar/banner. These are public
files with no user context, so the session/cookie/CSRF chain is unnecessary.
Also skips BotDetector (one analytics_bot_logs INSERT per image) and the SEO
redirect lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:28:18 +03:00

819 lines
57 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
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin;
use App\Http\Controllers\Admin\AnalyticsController as AdminAnalyticsController;
use App\Http\Controllers\MediaController;
use App\Http\Controllers\Frontend;
use App\Http\Controllers\Frontend\EmailVerificationController;
use App\Http\Controllers\Frontend\PasswordResetController;
use App\Http\Controllers\Frontend\CommentController;
use App\Http\Controllers\Frontend\ProfileController;
use App\Http\Controllers\Frontend\AiController;
use App\Http\Controllers\Frontend\TrackingController;
use App\Http\Controllers\Frontend\UserFeatureController;
use App\Http\Controllers\Frontend\SocialController;
use App\Http\Controllers\Frontend\TribunalController;
use App\Http\Controllers\Frontend\MessageController;
use App\Http\Controllers\Frontend\VoiceCallController;
use App\Http\Controllers\Frontend\DiscoverController;
if (! function_exists('animexe_url_is_safe')) {
/**
* SSRF guard for the server-side stream/subtitle proxies.
*
* Only http(s) URLs are allowed, and the host must resolve exclusively to
* public IPs. Resolving (instead of matching the hostname text) is what
* blocks link-local cloud metadata (169.254.169.254), loopback, private
* ranges and hostnames deliberately pointed at them (DNS rebinding).
*/
function animexe_url_is_safe(?string $url): bool
{
if (! $url) return false;
$scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
if (! in_array($scheme, ['http', 'https'], true)) return false;
$host = parse_url($url, PHP_URL_HOST);
if (! $host) return false;
$ips = [];
if (filter_var($host, FILTER_VALIDATE_IP)) {
$ips[] = $host;
} else {
foreach (@gethostbynamel($host) ?: [] as $ip) $ips[] = $ip;
foreach (@dns_get_record($host, DNS_AAAA) ?: [] as $rec) {
if (! empty($rec['ipv6'])) $ips[] = $rec['ipv6'];
}
}
if (! $ips) return false;
foreach ($ips as $ip) {
if (! filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return false;
}
}
return true;
}
}
// Public dosya servisi — kullanıcı bağlamı yok. Session/cookie middleware'i
// kapalı: yanıta Set-Cookie eklenmeyince Cloudflare bu görselleri edge'de
// cache'leyebiliyor (cookie'li yanıtı asla cache'lemez). Bot/SEO middleware'i
// de bir görsel için gereksiz DB işi yapıyordu.
Route::get('/media/{path}', [MediaController::class, 'show'])
->where('path', '.*')
->name('media.show')
->withoutMiddleware([
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class,
\App\Http\Middleware\BotDetector::class,
\App\Http\Middleware\SeoRedirectMiddleware::class,
]);
// ── Admin Login ──────────────────────────────────────────────────────────────
Route::prefix('admin')->name('admin.')->group(function () {
Route::get('login', [Admin\AuthController::class, 'showLogin'])->name('login');
Route::post('login', [Admin\AuthController::class, 'login'])->name('login.post');
Route::post('logout', [Admin\AuthController::class, 'logout'])->name('logout');
});
// ── Admin Panel ──────────────────────────────────────────────────────────────
// admin.access = auth kontrolü + admin veya moderatör rolü (permission yok)
// admin = sadece admin (veya moderatör + belirtilen permission)
Route::prefix('admin')->name('admin.')->middleware(['admin.access'])->group(function () {
// Dashboard — tüm moderatörlere açık
Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard');
// Anime
Route::resource('animes', Admin\AnimeController::class)->middleware(['admin:animes.view']);
Route::post('animes/{anime}/permissions', [Admin\AnimeController::class, 'updatePermissions'])->name('animes.permissions')->middleware(['admin:animes.edit']);
Route::post('animes/{anime}/fetch-mal-seasons', [Admin\AnimeController::class, 'fetchMalSeasons'])->name('animes.fetch-mal-seasons')->middleware(['admin:animes.edit']);
Route::post('animes/{anime}/fetch-mal', [Admin\AnimeController::class, 'fetchMalSingle'])->name('animes.fetch-mal')->middleware(['admin:animes.edit']);
Route::post('animes/bulk-find-mal', [Admin\AnimeController::class, 'bulkFindMal'])->name('animes.bulk-find-mal')->middleware(['admin:animes.edit']);
Route::post('animes/bulk-destroy', [Admin\AnimeController::class, 'bulkDestroy'])->name('animes.bulk-destroy')->middleware(['admin:animes.edit']);
Route::post('animes/destroy-zero-episodes', [Admin\AnimeController::class, 'destroyZeroEpisodes'])->name('animes.destroy-zero-episodes')->middleware(['admin']);
// Sezon
Route::resource('animes.seasons', Admin\SeasonController::class)->shallow()->middleware(['admin:episodes.view']);
// Bölüm
Route::post('episodes/bulk-intro', [Admin\EpisodeController::class, 'bulkIntro'])->name('episodes.bulk-intro')->middleware(['admin:episodes.edit']);
Route::post('episodes/bulk-destroy', [Admin\EpisodeController::class, 'bulkDestroy'])->name('episodes.bulk-destroy')->middleware(['admin:episodes.edit']);
Route::post('episodes/{episode}/scan-hevc', [Admin\EpisodeController::class, 'scanHevc'])->name('episodes.scan-hevc')->middleware(['admin:episodes.edit']);
Route::resource('episodes', Admin\EpisodeController::class)->middleware(['admin:episodes.view']);
// Reklamlar
Route::post('ads-settings', [Admin\AdController::class, 'saveSettings'])->name('ads.settings')->middleware(['admin']);
Route::post('ads/{ad}/toggle', [Admin\AdController::class, 'toggle'])->name('ads.toggle')->middleware(['admin']);
Route::resource('ads', Admin\AdController::class)->except(['create', 'show'])->middleware(['admin']);
// Kullanıcı
Route::resource('users', Admin\UserController::class)->middleware(['admin:users.view']);
Route::post('users/{user}/ban', [Admin\UserController::class, 'ban'])->name('users.ban')->middleware(['admin:users.ban']);
Route::post('users/{user}/unban', [Admin\UserController::class, 'unban'])->name('users.unban')->middleware(['admin:users.ban']);
Route::post('users/{user}/give-premium', [Admin\UserController::class, 'givePremium'])->name('users.give-premium')->middleware(['admin:users.premium']);
Route::post('users/{user}/remove-premium', [Admin\UserController::class, 'removePremium'])->name('users.remove-premium')->middleware(['admin:users.premium']);
// Yorum
Route::resource('comments', Admin\CommentController::class)->only(['index', 'show', 'destroy'])->middleware(['admin:comments.view']);
Route::post('comments/{comment}/approve', [Admin\CommentController::class, 'approve'])->name('comments.approve')->middleware(['admin:comments.approve']);
Route::post('comments/{comment}/reject', [Admin\CommentController::class, 'reject'])->name('comments.reject')->middleware(['admin:comments.approve']);
Route::post('comments/{comment}/pin', [Admin\CommentController::class, 'pin'])->name('comments.pin')->middleware(['admin:comments.pin']);
Route::post('comments/{comment}/reply', [Admin\CommentController::class, 'reply'])->name('comments.reply')->middleware(['admin:comments.approve']);
// Üyelik planları — sadece admin
Route::resource('plans', Admin\PlanController::class)->middleware(['admin']);
// Abonelikler — sadece admin
Route::resource('subscriptions', Admin\SubscriptionController::class)->only(['index', 'show', 'store', 'destroy'])->middleware(['admin']);
// Aktivasyon kodları — sadece admin
Route::prefix('activation-codes')->name('activation-codes.')->middleware(['admin'])->group(function () {
Route::get('/', [Admin\ActivationCodeController::class, 'index'])->name('index');
Route::post('/generate', [Admin\ActivationCodeController::class, 'generate'])->name('generate');
Route::get('/export', [Admin\ActivationCodeController::class, 'export'])->name('export');
Route::delete('/{activationCode}', [Admin\ActivationCodeController::class, 'destroy'])->name('destroy');
Route::post('/destroy-batch', [Admin\ActivationCodeController::class, 'destroyBatch'])->name('destroy-batch');
Route::post('/destroy-selected', [Admin\ActivationCodeController::class, 'destroySelected'])->name('destroy-selected');
});
// Türler
Route::resource('genres', Admin\GenreController::class)->middleware(['admin:genres.manage']);
// Banner
Route::resource('banners', Admin\BannerController::class)->middleware(['admin:banners.manage']);
// Sezon JSON (AJAX)
Route::get('animes/{anime}/seasons-json', function (\App\Models\Anime $anime) {
return response()->json($anime->seasons()->select('id', 'season_number')->get());
})->name('animes.seasons-json')->middleware(['admin:animes.view']);
// Global İzin Ayarları — sadece admin
Route::get('permissions', [Admin\PermissionController::class, 'index'])->name('permissions.index')->middleware(['admin']);
Route::post('permissions', [Admin\PermissionController::class, 'update'])->name('permissions.update')->middleware(['admin']);
// Ayarlar — sadece admin
Route::get('settings', [Admin\SettingController::class, 'index'])->name('settings.index')->middleware(['admin']);
Route::post('settings', [Admin\SettingController::class, 'update'])->name('settings.update')->middleware(['admin']);
Route::post('settings/intro-upload', [Admin\SettingController::class, 'uploadIntro'])->name('settings.intro.upload')->middleware(['admin']);
Route::post('settings/favicon-upload', [Admin\SettingController::class, 'uploadFavicon'])->name('settings.favicon.upload')->middleware(['admin']);
Route::post('settings/mail-test', [Admin\SettingController::class, 'testMail'])->name('settings.mail.test')->middleware(['admin']);
// Trend Yönetimi — sadece admin
Route::get('trending', [Admin\TrendingController::class, 'index'])->name('trending.index')->middleware(['admin']);
Route::post('trending/{anime}/toggle', [Admin\TrendingController::class, 'toggle'])->name('trending.toggle')->middleware(['admin']);
Route::post('trending/{anime}/move', [Admin\TrendingController::class, 'move'])->name('trending.move')->middleware(['admin']);
Route::post('trending/reorder', [Admin\TrendingController::class, 'reorder'])->name('trending.reorder')->middleware(['admin']);
Route::get('trending/search', [Admin\TrendingController::class, 'search'])->name('trending.search')->middleware(['admin']);
Route::post('trending/compute-scores', [Admin\TrendingController::class, 'computeScores'])->name('trending.compute-scores')->middleware(['admin']);
// AI — sadece admin
Route::post('ai/generate', [Admin\AiController::class, 'generate'])->name('ai.generate')->middleware(['admin']);
Route::post('ai/anime-meta', [Admin\AiController::class, 'animeMeta'])->name('ai.anime-meta')->middleware(['admin']);
Route::get('ai/descriptions', [Admin\AiController::class, 'descriptionsPage'])->name('ai.descriptions')->middleware(['admin']);
Route::post('ai/episode-ids', [Admin\AiController::class, 'episodeIds'])->name('ai.episode-ids')->middleware(['admin']);
Route::post('ai/fill-one', [Admin\AiController::class, 'fillOne'])->name('ai.fill-one')->middleware(['admin']);
Route::get('ai/anime-meta-bulk', [Admin\AiController::class, 'animeMetaPage'])->name('ai.anime-meta-bulk')->middleware(['admin']);
Route::post('ai/anime-meta-ids', [Admin\AiController::class, 'animeMetaIds'])->name('ai.anime-meta-ids')->middleware(['admin']);
Route::post('ai/fill-anime-meta', [Admin\AiController::class, 'fillAnimeMeta'])->name('ai.fill-anime-meta')->middleware(['admin']);
// Analitik — genel sadece admin, kullanıcı analitik permission ile
Route::get('analytics', [AdminAnalyticsController::class, 'index'])->name('analytics.index')->middleware(['admin:analytics.view']);
Route::post('analytics/block-ip', [AdminAnalyticsController::class, 'blockIp'])->name('analytics.block-ip')->middleware(['admin:analytics.block']);
Route::post('analytics/unblock-ip',[AdminAnalyticsController::class, 'unblockIp'])->name('analytics.unblock-ip')->middleware(['admin:analytics.block']);
Route::get('stats', [\App\Http\Controllers\Admin\ContentStatsController::class, 'index'])->name('stats.index')->middleware(['admin']);
Route::get('health', [\App\Http\Controllers\Admin\HealthController::class, 'index'])->name('health.index')->middleware(['admin']);
Route::delete('health/anime/{anime}', [\App\Http\Controllers\Admin\HealthController::class, 'deleteAnime'])->name('health.delete-anime')->middleware(['admin']);
Route::post('health/anime/{anime}/delete-source', [\App\Http\Controllers\Admin\HealthController::class, 'deleteSourceEpisodes'])->name('health.delete-source-episodes')->middleware(['admin']);
Route::get('health/storage-stats', [\App\Http\Controllers\Admin\HealthController::class, 'storageStats'])->name('health.storage-stats')->middleware(['admin']);
Route::post('health/cleanup-storage', [\App\Http\Controllers\Admin\HealthController::class, 'cleanupStorage'])->name('health.cleanup-storage')->middleware(['admin']);
Route::get('health/session-info', [\App\Http\Controllers\Admin\HealthController::class, 'sessionInfo'])->name('health.session-info')->middleware(['admin']);
// Anime istekleri
Route::get('anime-requests', [\App\Http\Controllers\Admin\AnimeRequestController::class, 'index'])->name('anime-requests.index')->middleware(['admin:requests.manage']);
Route::patch('anime-requests/{animeRequest}', [\App\Http\Controllers\Admin\AnimeRequestController::class, 'update'])->name('anime-requests.update')->middleware(['admin:requests.manage']);
Route::delete('anime-requests/{animeRequest}', [\App\Http\Controllers\Admin\AnimeRequestController::class, 'destroy'])->name('anime-requests.destroy')->middleware(['admin:requests.manage']);
// Bildirim yönetimi
Route::get('notifications', [\App\Http\Controllers\Admin\NotificationController::class, 'index'])->name('notifications.index')->middleware(['admin:notifications.view']);
Route::post('notifications/send', [\App\Http\Controllers\Admin\NotificationController::class, 'send'])->name('notifications.send')->middleware(['admin:notifications.send']);
// Moderatör Yönetimi — sadece admin
Route::middleware(['admin'])->group(function () {
Route::get('moderators', [\App\Http\Controllers\Admin\ModeratorController::class, 'index'])->name('moderators.index');
Route::post('moderators/promote', [\App\Http\Controllers\Admin\ModeratorController::class, 'promote'])->name('moderators.promote');
Route::post('moderators/{user}/demote', [\App\Http\Controllers\Admin\ModeratorController::class, 'demote'])->name('moderators.demote');
Route::get('moderators/{user}/edit', [\App\Http\Controllers\Admin\ModeratorController::class, 'edit'])->name('moderators.edit');
Route::post('moderators/{user}/permissions', [\App\Http\Controllers\Admin\ModeratorController::class, 'savePermissions'])->name('moderators.permissions');
Route::post('moderators/{user}/toggle-permission', [\App\Http\Controllers\Admin\ModeratorController::class, 'togglePermission'])->name('moderators.toggle-permission');
});
// Kullanıcı Analitiği
Route::get('analytics/users', [\App\Http\Controllers\Admin\UserAnalyticsController::class, 'index'])->name('analytics.users')->middleware(['admin:analytics.users']);
Route::get('analytics/users/{user}', [\App\Http\Controllers\Admin\UserAnalyticsController::class, 'userDetail'])->name('analytics.user-detail')->middleware(['admin:analytics.users']);
// Mobil Uygulama Yönetimi — sadece admin
Route::get('mobile-app', [\App\Http\Controllers\Admin\MobileAppController::class, 'index'])->name('mobile.index')->middleware(['admin']);
Route::post('mobile-app', [\App\Http\Controllers\Admin\MobileAppController::class, 'update'])->name('mobile.update')->middleware(['admin']);
// Import (Anime çekme)
Route::get('import', [Admin\ImportController::class, 'index'])->name('import.index')->middleware(['admin:import.manage']);
Route::post('import', [Admin\ImportController::class, 'store'])->name('import.store')->middleware(['admin:import.manage']);
Route::get('import/{import}', [Admin\ImportController::class, 'show'])->name('import.show')->middleware(['admin:import.manage']);
Route::delete('import/{import}', [Admin\ImportController::class, 'destroy'])->name('import.destroy')->middleware(['admin:import.manage']);
Route::delete('import-failed', [Admin\ImportController::class, 'destroyFailed'])->name('import.destroyFailed')->middleware(['admin:import.manage']);
Route::delete('import-pending', [Admin\ImportController::class, 'destroyPending'])->name('import.destroyPending')->middleware(['admin:import.manage']);
Route::delete('import-stuck', [Admin\ImportController::class, 'destroyStuck'])->name('import.destroyStuck')->middleware(['admin:import.manage']);
Route::delete('import-by-status', [Admin\ImportController::class, 'destroyByStatus'])->name('import.destroyByStatus')->middleware(['admin:import.manage']);
Route::get('import-bulk-counts', [Admin\ImportController::class, 'bulkCounts'])->name('import.bulkCounts')->middleware(['admin:import.manage']);
// Import araçları (terminal gerektirmez)
Route::post('import-tools/fix-subtitles', [Admin\ImportController::class, 'fixSubtitles'])->name('import.fixSubtitles')->middleware(['admin:import.manage']);
Route::post('import-tools/requeue-anizium', [Admin\ImportController::class, 'requeueAnizium'])->name('import.requeueAnizium')->middleware(['admin:import.manage']);
Route::post('import-tools/requeue-animecix', [Admin\ImportController::class, 'requeueAnimecix'])->name('import.requeueAnimecix')->middleware(['admin:import.manage']);
Route::get('import-tools/source-stats', [Admin\ImportController::class, 'sourceStats'])->name('import.sourceStats')->middleware(['admin:import.manage']);
// SEO Paneli — sadece admin
Route::middleware(['admin'])->group(function () {
Route::get('seo', [\App\Http\Controllers\Admin\SeoController::class, 'index'])->name('seo.index');
Route::post('seo', [\App\Http\Controllers\Admin\SeoController::class, 'update'])->name('seo.update');
Route::post('seo/robots', [\App\Http\Controllers\Admin\SeoController::class, 'updateRobots'])->name('seo.robots');
Route::post('seo/ping', [\App\Http\Controllers\Admin\SeoController::class, 'pingSearchEngines'])->name('seo.ping');
Route::get('seo/audit', [\App\Http\Controllers\Admin\SeoController::class, 'auditJson'])->name('seo.audit');
Route::post('seo/keywords', [\App\Http\Controllers\Admin\SeoController::class, 'storeKeyword'])->name('seo.keywords.store');
Route::delete('seo/keywords/{keyword}', [\App\Http\Controllers\Admin\SeoController::class, 'destroyKeyword'])->name('seo.keywords.destroy');
Route::post('seo/redirects', [\App\Http\Controllers\Admin\SeoController::class, 'storeRedirect'])->name('seo.redirects.store');
Route::delete('seo/redirects/{redirect}', [\App\Http\Controllers\Admin\SeoController::class, 'destroyRedirect'])->name('seo.redirects.destroy');
Route::post('seo/redirects/{redirect}/toggle', [\App\Http\Controllers\Admin\SeoController::class, 'toggleRedirect'])->name('seo.redirects.toggle');
Route::post('seo/anime-bulk', [\App\Http\Controllers\Admin\SeoController::class, 'bulkSaveAnime'])->name('seo.anime-bulk');
Route::post('seo/anime/{anime}/generate', [\App\Http\Controllers\Admin\SeoController::class, 'generateAnimeSeo'])->name('seo.anime.generate');
Route::post('seo/anime-bulk-generate', [\App\Http\Controllers\Admin\SeoController::class, 'bulkGenerateAllSeo'])->name('seo.anime.bulk-generate');
Route::post('seo/ai-bulk-generate', [\App\Http\Controllers\Admin\SeoController::class, 'aiBulkGenerateSeo'])->name('seo.ai.bulk-generate');
Route::post('seo/bulk-fill-batch', [\App\Http\Controllers\Admin\SeoController::class, 'bulkFillBatch'])->name('seo.bulk-fill-batch');
Route::get('seo/bulk-fill-stats', [\App\Http\Controllers\Admin\SeoController::class, 'bulkFillStats'])->name('seo.bulk-fill-stats');
Route::post('seo/pagespeed', [\App\Http\Controllers\Admin\SeoController::class, 'pagespeedCheck'])->name('seo.pagespeed');
Route::get('seo/internal-links', [\App\Http\Controllers\Admin\SeoController::class, 'internalLinksAudit'])->name('seo.internal-links');
Route::get('seo/duplicate-content', [\App\Http\Controllers\Admin\SeoController::class, 'duplicateContent'])->name('seo.duplicate-content');
Route::post('seo/ai/chat', [\App\Http\Controllers\Admin\SeoController::class, 'aiChat'])->name('seo.ai.chat');
Route::post('seo/ai/anime/{anime}/generate', [\App\Http\Controllers\Admin\SeoController::class, 'aiGenerateAnimeSeo'])->name('seo.ai.anime-generate');
Route::post('seo/ai/keyword-suggest', [\App\Http\Controllers\Admin\SeoController::class, 'aiKeywordSuggest'])->name('seo.ai.keyword-suggest');
Route::post('seo/ai/page-analysis', [\App\Http\Controllers\Admin\SeoController::class, 'aiPageAnalysis'])->name('seo.ai.page-analysis');
Route::post('seo/ai/faq-schema', [\App\Http\Controllers\Admin\SeoController::class, 'aiFaqSchema'])->name('seo.ai.faq-schema');
Route::post('seo/ai/content-strategy', [\App\Http\Controllers\Admin\SeoController::class, 'aiContentStrategy'])->name('seo.ai.content-strategy');
Route::post('seo/ai/robots-txt', [\App\Http\Controllers\Admin\SeoController::class, 'aiRobotsTxt'])->name('seo.ai.robots-txt');
});
// Blog Yönetimi — sadece admin
Route::middleware(['admin'])->group(function () {
Route::get('blog', [\App\Http\Controllers\Admin\BlogController::class, 'index'])->name('blog.index');
Route::get('blog/create', [\App\Http\Controllers\Admin\BlogController::class, 'create'])->name('blog.create');
Route::post('blog', [\App\Http\Controllers\Admin\BlogController::class, 'store'])->name('blog.store');
Route::get('blog/{blog}/edit', [\App\Http\Controllers\Admin\BlogController::class, 'edit'])->name('blog.edit');
Route::put('blog/{blog}', [\App\Http\Controllers\Admin\BlogController::class, 'update'])->name('blog.update');
Route::delete('blog/{blog}', [\App\Http\Controllers\Admin\BlogController::class, 'destroy'])->name('blog.destroy');
Route::post('blog/generate-ai', [\App\Http\Controllers\Admin\BlogController::class, 'generateAi'])->name('blog.generate-ai');
Route::post('blog/bulk-generate', [\App\Http\Controllers\Admin\BlogController::class, 'bulkGenerate'])->name('blog.bulk-generate');
});
});
// ── Sitemap ──────────────────────────────────────────────────────────────────
Route::get('/sitemap.xml', [\App\Http\Controllers\SitemapController::class, 'index'])->name('sitemap');
Route::get('/sitemap-main.xml', [\App\Http\Controllers\SitemapController::class, 'main'])->name('sitemap.main');
Route::get('/sitemap-animes.xml', [\App\Http\Controllers\SitemapController::class, 'animes'])->name('sitemap.animes');
Route::get('/sitemap-videos.xml', [\App\Http\Controllers\SitemapController::class, 'videos'])->name('sitemap.videos');
Route::get('/sitemap-blog.xml', [\App\Http\Controllers\SitemapController::class, 'blog'])->name('sitemap.blog');
// ── Frontend ─────────────────────────────────────────────────────────────────
Route::get('/', [Frontend\HomeController::class, 'index'])->name('home');
Route::get('/search', [Frontend\HomeController::class, 'search'])->name('search');
Route::get('/search/suggest', [Frontend\HomeController::class, 'searchSuggest'])->name('search.suggest');
Route::get('/genre/{genre:slug}', [Frontend\HomeController::class, 'genre'])->name('genre');
Route::get('/anime/{anime:slug}', [Frontend\AnimeController::class, 'show'])->name('anime.show');
Route::get('/watch/{anime:slug}/{season}/{episode}', [Frontend\PlayerController::class, 'watch'])->where(['season' => '[0-9]+', 'episode' => '[0-9]+'])->name('watch')->middleware('secure.player');
// Keşfet (Tinder-style discovery)
Route::get('/kesfet', [DiscoverController::class, 'index'])->name('discover');
Route::get('/kesfet/cards', [DiscoverController::class, 'cards'])->name('discover.cards');
Route::post('/kesfet/swipe', [DiscoverController::class, 'swipe'])->name('discover.swipe');
Route::post('/kesfet/reset', [DiscoverController::class, 'reset'])->name('discover.reset');
Route::get('/kesfet/results', [DiscoverController::class, 'results'])->name('discover.results');
// Blog
Route::get('/blog', [\App\Http\Controllers\Frontend\BlogController::class, 'index'])->name('blog.index');
Route::get('/blog/{slug}', [\App\Http\Controllers\Frontend\BlogController::class, 'show'])->name('blog.show');
// Kullanıcı arama (share modal için)
Route::middleware('auth')->get('/api/users/search', function(\Illuminate\Http\Request $req) {
$q = trim($req->query('q', ''));
if (strlen($q) < 2) return response()->json([]);
$me = auth()->id();
$users = \App\Models\User::where('id', '!=', $me)
->where(fn($query) => $query->where('name','like',"%{$q}%")->orWhere('username','like',"%{$q}%"))
->limit(8)
->get()
->map(fn($u) => [
'id' => $u->id,
'name' => $u->name,
'username' => $u->username,
'avatar' => $u->avatar ? \App\Support\MediaUrl::fromStoragePath($u->avatar) : null,
]);
return response()->json($users);
})->name('api.users.search');
// Public profil
Route::get('/u/{user}', [Frontend\ProfileController::class, 'publicProfile'])->name('user.profile');
Route::get('/u/{user}/compatibility', [SocialController::class, 'compatibility'])->name('user.compatibility');
Route::get('/u/{user}/card', [SocialController::class, 'card'])->name('user.card');
// Watch Party (public read, auth write)
Route::get('/party/{roomCode}', [SocialController::class, 'partyShow'])->name('watch.party');
// NicoNico timestamp yorumları (okuma herkese açık)
Route::get('/episode/{episode}/timestamp-comments', [SocialController::class, 'timestampComments'])->name('episode.timestamp-comments');
// Tahmin sayısı (herkese açık)
Route::get('/episode/{episode}/predictions', [SocialController::class, 'predictions'])->name('episode.predictions');
// İlk kez izleyenler sayısı (herkese açık)
Route::get('/episode/{episode}/first-watch-count', [SocialController::class, 'firstWatchCount'])->name('episode.first-watch-count');
// Premium / Planlar
Route::get('/premium', [\App\Http\Controllers\Frontend\PremiumController::class, 'plans'])->name('premium.plans');
// Aktivasyon kodu
Route::get('/premium/activate', [\App\Http\Controllers\Frontend\ActivationController::class, 'show'])->name('premium.activate');
Route::middleware('auth')->post('/premium/activate', [\App\Http\Controllers\Frontend\ActivationController::class, 'redeem'])->name('premium.activate.redeem');
// Checkout (ödeme — iyzico, ileride kullanılabilir)
Route::middleware('auth')->group(function () {
Route::get('/checkout/{plan}', [\App\Http\Controllers\Frontend\CheckoutController::class, 'show'])->name('checkout.show');
Route::post('/checkout/{plan}', [\App\Http\Controllers\Frontend\CheckoutController::class, 'initialize'])->name('checkout.initialize');
Route::get('/checkout/success', [\App\Http\Controllers\Frontend\CheckoutController::class, 'success'])->name('checkout.success');
Route::get('/checkout/failed', [\App\Http\Controllers\Frontend\CheckoutController::class, 'failed'])->name('checkout.failed');
});
Route::post('/checkout/callback', [\App\Http\Controllers\Frontend\CheckoutController::class, 'callback'])->name('checkout.callback');
// ── Frontend — Auth required ──────────────────────────────────────────────────
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
Route::get('/profile/settings', [ProfileController::class, 'settings'])->name('profile.settings');
Route::post('/profile/settings', [ProfileController::class, 'update'])->name('profile.update');
Route::post('/profile/avatar', [ProfileController::class, 'updateAvatar'])->name('profile.avatar');
Route::post('/profile/banner', [ProfileController::class, 'updateBanner'])->name('profile.banner');
Route::post('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
Route::post('/profile/premium-cosmetics', [\App\Http\Controllers\Frontend\PremiumController::class, 'saveCosmetics'])->name('premium.cosmetics.save');
Route::post('/comments', [CommentController::class, 'store'])->name('comments.store')->middleware('throttle:12,1');
Route::post('/comments/{comment}/like', [CommentController::class, 'like'])->name('comments.like')->middleware('throttle:60,1');
// AI Hub
Route::get('/ai', [AiController::class, 'index'])->name('ai.index');
// Kullanıcı özellikleri
Route::get('/watchlist', [UserFeatureController::class, 'watchlistIndex'])->name('watchlist');
Route::get('/watchlist/export', [UserFeatureController::class, 'watchlistExport'])->name('watchlist.export');
Route::post('/watchlist/{anime}/toggle', [UserFeatureController::class, 'watchlistToggle'])->name('watchlist.toggle');
Route::post('/episode/{episode}/vote', [UserFeatureController::class, 'episodeVote'])->name('episode.vote');
Route::post('/anime/{anime}/rate', [UserFeatureController::class, 'animeRate'])->name('anime.rate');
Route::post('/continue-watching', [UserFeatureController::class, 'continueWatchingUpdate'])->name('continue.update');
// Anime istekleri (store + vote auth gerektirir)
Route::post('/anime-request', [UserFeatureController::class, 'requestStore'])->name('anime.request.store');
Route::post('/anime-request/{animeRequest}/vote', [UserFeatureController::class, 'requestVote'])->name('anime.request.vote');
// Takip
Route::post('/anime/{anime:slug}/follow', [UserFeatureController::class, 'followToggle'])->name('anime.follow');
// Kullanıcı takip
Route::post('/u/{user}/follow', [SocialController::class, 'followToggle'])->name('user.follow');
// Mesajlar
Route::get('/messages', [MessageController::class, 'index'])->name('messages.index');
Route::get('/messages/conversations', [MessageController::class, 'conversationsJson'])->name('messages.conversations');
Route::post('/messages/quick-share', [MessageController::class, 'quickShare'])->name('messages.quick-share');
Route::get('/messages/{conversation}', [MessageController::class, 'show'])->name('messages.show');
Route::post('/messages/{conversation}/send', [MessageController::class, 'send'])->name('messages.send')->middleware('throttle:30,1');
Route::get('/messages/{conversation}/poll', [MessageController::class, 'poll'])->name('messages.poll');
Route::post('/messages/start/{user}', [MessageController::class, 'startOrOpen'])->name('messages.start');
Route::post('/messages/upload-image', [MessageController::class, 'uploadImage'])->name('messages.upload-image')->middleware('throttle:20,1');
// Sesli Arama
Route::post('/calls/initiate', [VoiceCallController::class, 'initiate'])->name('calls.initiate')->middleware('throttle:10,1');
Route::post('/calls/{call}/answer', [VoiceCallController::class, 'answer'])->name('calls.answer');
Route::post('/calls/{call}/decline', [VoiceCallController::class, 'decline'])->name('calls.decline');
Route::post('/calls/{call}/end', [VoiceCallController::class, 'end'])->name('calls.end');
Route::get('/calls/poll', [VoiceCallController::class, 'poll'])->name('calls.poll');
// ── Sosyal Özellikler (auth gerektirir) ──────────────────────────────────
// NicoNico yorum yaz
Route::post('/episode/{episode}/timestamp-comments', [SocialController::class, 'timestampCommentStore'])->name('episode.timestamp-comment.store')->middleware('throttle:10,1');
// Tahmin oyunu
Route::post('/episode/{episode}/predictions', [SocialController::class, 'predictionStore'])->name('episode.prediction.store')->middleware('throttle:5,1');
Route::post('/predictions/{prediction}/vote', [SocialController::class, 'predictionVote'])->name('prediction.vote')->middleware('throttle:30,1');
// Watch Party
Route::post('/party/create', [SocialController::class, 'partyCreate'])->name('party.create');
Route::post('/party/{roomCode}/join', [SocialController::class, 'partyJoin'])->name('party.join');
Route::post('/party/{roomCode}/sync', [SocialController::class, 'partySync'])->name('party.sync');
Route::post('/party/{roomCode}/leave', [SocialController::class, 'partyLeave'])->name('party.leave');
// İlk kez izleyenler — kayıt
Route::post('/episode/{episode}/first-watch', [SocialController::class, 'firstWatchRegister'])->name('episode.first-watch');
// Bildirimler
Route::get('/notifications', [UserFeatureController::class, 'notificationsIndex'])->name('notifications');
// Bölüm notları
Route::post('/episode/{episode}/note', [UserFeatureController::class, 'noteStore'])->name('episode.note.store');
Route::delete('/notes/{note}', [UserFeatureController::class, 'noteDelete'])->name('episode.note.delete');
Route::get('/episode/{episode}/notes', [UserFeatureController::class, 'episodeNotesList'])->name('episode.notes.list');
Route::post('/ai/chat', [AiController::class, 'chat'])->name('ai.chat')->middleware('throttle:25,1');
Route::post('/ai/recommend', [AiController::class, 'recommend'])->name('ai.recommend')->middleware('throttle:20,1');
Route::post('/ai/search', [AiController::class, 'search'])->name('ai.search')->middleware('throttle:30,1');
Route::post('/ai/episode-info', [AiController::class, 'episodeInfo'])->name('ai.episode-info')->middleware('throttle:20,1');
Route::post('/ai/similar', [AiController::class, 'similar'])->name('ai.similar')->middleware('throttle:20,1');
});
// Anime istekleri listesi (herkese açık)
Route::get('/anime-request', [UserFeatureController::class, 'requestIndex'])->name('anime.request');
// Bildirim sayısı (AJAX, herkese açık — auth kontrolü controller'da)
Route::get('/notifications/count', [UserFeatureController::class, 'notificationsCount'])->name('notifications.count');
Route::get('/messages/unread-count', [MessageController::class, 'unreadCount'])->name('messages.unread-count');
Route::get('/comments', [CommentController::class, 'index'])->name('comments.index');
// ── Tracking (no auth required) ───────────────────────────────────────────────
Route::post('/track/pageview', [TrackingController::class, 'pageview'])->name('track.pageview');
Route::post('/track/watch', [TrackingController::class, 'watch'])->name('track.watch');
Route::post('/track/session-end', [TrackingController::class, 'sessionEnd'])->name('track.session-end');
Route::get('/comments/gif-search', [CommentController::class, 'gifSearch'])->name('comments.gif-search');
// ── Frontend Auth ─────────────────────────────────────────────────────────────
Route::get('/login', [Frontend\AuthController::class, 'showLogin'])->name('frontend.login')->middleware('guest');
Route::post('/login', [Frontend\AuthController::class, 'login'])->middleware(['guest', 'throttle:10,1']);
Route::get('/register', [Frontend\AuthController::class, 'showRegister'])->name('frontend.register')->middleware('guest');
Route::post('/register',[Frontend\AuthController::class, 'register'])->middleware(['guest', 'throttle:5,1']);
Route::post('/logout', [Frontend\AuthController::class, 'logout'])->name('frontend.logout')->middleware('auth');
// ── Social Auth ───────────────────────────────────────────────────────────────
Route::get('/auth/{provider}', [Frontend\AuthController::class, 'socialRedirect'])->name('social.redirect')->middleware('guest');
Route::get('/auth/{provider}/callback', [Frontend\AuthController::class, 'socialCallback'])->name('social.callback');
// ── Şifre Sıfırlama ──────────────────────────────────────────────────────────
Route::get('/forgot-password', [Frontend\PasswordResetController::class, 'showForgot'])->name('password.request')->middleware('guest');
Route::post('/forgot-password', [Frontend\PasswordResetController::class, 'sendResetLink'])->name('password.email')->middleware(['guest','throttle:5,1']);
Route::get('/reset-password/{token}', [Frontend\PasswordResetController::class, 'showReset'])->name('password.reset')->middleware('guest');
Route::post('/reset-password', [Frontend\PasswordResetController::class, 'reset'])->name('password.update')->middleware(['guest','throttle:5,1']);
// ── E-posta Doğrulama ────────────────────────────────────────────────────────
Route::get('/email/verify', [Frontend\EmailVerificationController::class, 'notice'])->name('verification.notice')->middleware('auth');
Route::get('/email/verify/{id}/{hash}', [Frontend\EmailVerificationController::class, 'verify'])->name('verification.verify')->middleware(['auth','signed']);
Route::post('/email/verification-notification', [Frontend\EmailVerificationController::class, 'resend'])->name('verification.send')->middleware(['auth','throttle:3,1']);
// HLS Stream Proxy - CORS bypass for external m3u8 sources
Route::get("/stream/proxy", function (\Illuminate\Http\Request $request) {
$u = $request->query("u", "");
$url = base64_decode($u, true);
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
// SSRF guard — resolves the host and rejects private/reserved/link-local IPs
if (! animexe_url_is_safe($url)) abort(403);
$host = parse_url($url, PHP_URL_HOST) ?? "";
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
// Referer: custom ref param > auto-detect from URL's own origin
$customRef = $request->query("ref", "");
if ($customRef) {
$decoded = base64_decode($customRef, true);
$referer = ($decoded && filter_var($decoded, FILTER_VALIDATE_URL)) ? rtrim($decoded, '/') . '/' : null;
}
if (empty($referer)) {
// Bilinen CDN'ler için doğru Referer'ı zorla — parametre gelmese bile
$knownRefs = [
'aniziumserver.sbs' => 'https://anizium.co/',
'aniziumserver.site' => 'https://anizium.co/',
'aniziumserver.com' => 'https://anizium.co/',
];
$referer = null;
foreach ($knownRefs as $domain => $ref) {
if ($host === $domain || str_ends_with($host, '.' . $domain)) {
$referer = $ref;
break;
}
}
if (!$referer) $referer = $scheme . '://' . $host . '/';
$customRef = base64_encode($referer);
}
$origin = rtrim($referer, '/');
// Tek istek yürüten yardımcı — farklı header setleriyle tekrar denemek için.
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
$fetchUpstream = function (string $url, array $headers) {
$hops = 0;
while (true) {
if (! animexe_url_is_safe($url)) return ['', 0, '', 'blocked_by_ssrf_guard'];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_ENCODING => '',
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_HTTPHEADER => $headers,
]);
$content = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
$errNo = curl_errno($ch);
$errStr = $errNo ? curl_strerror($errNo) : '';
curl_close($ch);
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
$hops++;
$url = $location;
continue;
}
return [$content, $httpCode, $ctype, $errStr];
}
};
$UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
// 1) Referer-only (gerçek oynatıcı davranışı — Origin göndermez, hotlink koruması daha toleranslı)
[$content, $httpCode, $contentType, $curlErrStr] = $fetchUpstream($url, [
"Referer: {$referer}",
"User-Agent: {$UA}",
"Accept: */*",
]);
// 2) Başarısızsa Origin + Sec-Fetch header'larıyla (tarayıcı taklidi) tekrar dene
if (!$content || $httpCode < 200 || $httpCode >= 400) {
[$content, $httpCode, $contentType, $curlErrStr] = $fetchUpstream($url, [
"Referer: {$referer}",
"Origin: {$origin}",
"User-Agent: {$UA}",
"Accept: */*",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: cross-site",
]);
}
if (!$content || $httpCode < 200 || $httpCode >= 400) {
\Illuminate\Support\Facades\Log::warning('stream_proxy_fail', [
'url' => $url,
'http' => $httpCode,
'curl' => $curlErrStr,
'body' => substr((string)$content, 0, 300),
]);
abort(502);
}
$path = strtolower(parse_url($url, PHP_URL_PATH) ?? "");
$isM3u8 = str_ends_with($path, ".m3u8")
|| str_contains($contentType, "mpegurl")
|| str_starts_with(trim((string)$content), "#EXTM3U");
if ($isM3u8) {
$baseUrl = rtrim(dirname($url), "/");
$refSuffix = "&ref=" . urlencode($customRef);
$proxyBase = url("/stream/proxy") . "?u=";
$segBase = url("/stream/seg") . "?u=";
$lines = explode("\n", str_replace("\r\n", "\n", (string)$content));
$out = [];
foreach ($lines as $line) {
$t = rtrim($line);
if ($t === "") { $out[] = ""; continue; }
if (str_starts_with($t, "#")) {
// URI="..." attribute → şifreleme anahtarları proxy'den geçsin
$t = preg_replace_callback('/URI="([^"]+)"/', function ($m) use ($baseUrl, $proxyBase, $refSuffix) {
$abs = str_starts_with($m[1], "http") ? $m[1] : $baseUrl . "/" . ltrim($m[1], "/");
return "URI=\"" . $proxyBase . base64_encode($abs) . $refSuffix . "\"";
}, $t);
$out[] = $t;
} else {
$abs = str_starts_with($t, "http") ? $t : $baseUrl . "/" . ltrim($t, "/");
$absHost = parse_url($abs, PHP_URL_HOST) ?? "";
$absPath = strtolower(parse_url($abs, PHP_URL_PATH) ?? "");
// Sub-playlist (.m3u8) → manifest proxy (kendi segment URL'lerini de düzeltir, ref aktarılır)
if (str_ends_with($absPath, ".m3u8")) {
$out[] = $proxyBase . base64_encode($abs) . $refSuffix;
} else {
// BunnyCDN, animexe CDN → direkt (CORS destekli)
// Diğerleri → /stream/seg (Referer spoofing + önbellekli)
$isTrusted = str_ends_with($absHost, "b-cdn.net")
|| str_ends_with($absHost, "animexe.com");
$out[] = $isTrusted ? $abs : $segBase . base64_encode($abs) . $refSuffix;
}
}
}
return response(implode("\n", $out), 200, [
"Content-Type" => "application/vnd.apple.mpegurl; charset=utf-8",
"Access-Control-Allow-Origin" => "*",
"Cache-Control" => "no-cache, no-store",
]);
}
// Segment proxy: Content-Type düzeltmesi
// .png uzantılı ama aslında video segmenti olan dosyaları düzelt
$segCt = $contentType;
if (str_starts_with($segCt, "image/") || $segCt === "application/octet-stream") {
$segCt = "video/mp2t";
}
return response($content, 200, [
"Content-Type" => $segCt ?: "video/mp2t",
"Access-Control-Allow-Origin" => "*",
"Cache-Control" => "public, max-age=3600",
]);
})->name("stream.proxy");
// Segment proxy — önbellekli, Content-Type düzeltmeli (.png → video/mp2t)
// İlk istekte CDN'den çekip diske kaydeder, sonraki istekler PHP'yi atlatır
Route::get("/stream/seg", function (\Illuminate\Http\Request $request) {
$u = $request->query("u", "");
$url = base64_decode($u, true);
if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) abort(400);
// SSRF guard — resolves the host and rejects private/reserved/link-local IPs
if (! animexe_url_is_safe($url)) abort(403);
$host = parse_url($url, PHP_URL_HOST) ?? "";
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
// Referer: custom ref param > auto-detect from URL's own origin
$customRef = $request->query("ref", "");
if ($customRef) {
$decoded = base64_decode($customRef, true);
$referer = ($decoded && filter_var($decoded, FILTER_VALIDATE_URL)) ? rtrim($decoded, '/') . '/' : null;
}
if (empty($referer)) {
$referer = $scheme . '://' . $host . '/';
}
$origin = rtrim($referer, '/');
$cacheDir = storage_path('app/seg_cache');
if (!is_dir($cacheDir)) @mkdir($cacheDir, 0755, true);
$cachePath = $cacheDir . '/' . md5($url) . '.ts';
// Önbellekte varsa direkt sun
if (file_exists($cachePath)) {
$size = filesize($cachePath);
return response(file_get_contents($cachePath), 200, [
'Content-Type' => 'video/mp2t',
'Content-Length' => $size,
'Access-Control-Allow-Origin' => '*',
'Cache-Control' => 'public, max-age=86400',
]);
}
// CDN'den çek — önce Referer-only (oynatıcı taklidi), olmazsa Origin ekle.
// Redirect'ler elle takip edilir ki her adım SSRF kontrolünden geçsin.
$segFetch = function (string $url, array $headers) {
$hops = 0;
while (true) {
if (! animexe_url_is_safe($url)) return ['', 0];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_ENCODING => '',
CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
CURLOPT_HTTPHEADER => $headers,
]);
$content = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$location = (string) curl_getinfo($ch, CURLINFO_REDIRECT_URL);
curl_close($ch);
if ($httpCode >= 300 && $httpCode < 400 && $location !== '' && $hops < 5) {
$hops++;
$url = $location;
continue;
}
return [$content, $httpCode];
}
};
$segUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
[$content, $httpCode] = $segFetch($url, [
"Referer: {$referer}",
"User-Agent: {$segUA}",
"Accept: */*",
]);
if (!$content || $httpCode < 200 || $httpCode >= 400) {
[$content, $httpCode] = $segFetch($url, [
"Referer: {$referer}",
"Origin: {$origin}",
"User-Agent: {$segUA}",
"Accept: */*",
"Sec-Fetch-Dest: empty",
"Sec-Fetch-Mode: cors",
"Sec-Fetch-Site: cross-site",
]);
}
if (!$content || $httpCode < 200 || $httpCode >= 400) abort(502);
// Diske kaydet (sadece dosya varsa — inode kontrolü)
$segFiles = glob("{$cacheDir}/*.ts") ?: [];
if (count($segFiles) < 300) {
// Max 300 segment tutuyoruz — inode patlamasını önler
file_put_contents($cachePath, $content);
}
// %10 ihtimalle temizlik: 1 saatten eski segmentleri sil
if (rand(1, 10) === 1) {
foreach (glob("{$cacheDir}/*.ts") ?: [] as $f) {
if (filemtime($f) < time() - 3600) @unlink($f);
}
// Max 300 üzerindeyse en eskileri sil
$files = glob("{$cacheDir}/*.ts") ?: [];
if (count($files) > 300) {
usort($files, fn($a, $b) => filemtime($a) - filemtime($b));
foreach (array_slice($files, 0, count($files) - 300) as $f) @unlink($f);
}
}
return response($content, 200, [
'Content-Type' => 'video/mp2t',
'Content-Length' => strlen($content),
'Access-Control-Allow-Origin' => '*',
'Cache-Control' => 'public, max-age=86400',
]);
})->name('stream.seg');
// VTT Proxy (CORS bypass for subtitle files)
Route::get('/vtt-proxy', function (\Illuminate\Http\Request $request) {
$url = $request->query('url', '');
if (!filter_var($url, FILTER_VALIDATE_URL)) abort(400);
$host = parse_url($url, PHP_URL_HOST) ?? '';
$allowed = ['b-cdn.net', 'bunnycdn.com', 'aniziumserver.site', 'aniziumserver.com', 'aniziumserver.sbs', 'anizium.co'];
$ok = false;
// Tam eşleşme veya gerçek alt-domain — "evilanizium.co" gibi sahte son ekler geçmez
foreach ($allowed as $a) { if ($host === $a || str_ends_with($host, '.' . $a)) { $ok = true; break; } }
if (!$ok) abort(403);
try {
$resp = \Illuminate\Support\Facades\Http::timeout(15)
->withHeaders([
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept' => 'text/vtt,text/plain,*/*',
'Accept-Language' => 'tr-TR,tr;q=0.9,en;q=0.8',
'Referer' => 'https://x.anizium.co/',
])
->get($url);
if (!$resp->successful()) abort($resp->status());
$body = $resp->body();
// Cloudflare / Anizium hata sayfası dönmüşse (WEBVTT içermiyor) 502
if (!str_contains(substr($body, 0, 50), 'WEBVTT')) abort(502);
return response($body, 200)
->header('Content-Type', 'text/vtt; charset=utf-8')
->header('Cache-Control', 'public, max-age=3600')
->header('Access-Control-Allow-Origin', '*');
} catch (\Exception $e) { abort(502); }
})->name('vtt.proxy');
// ── Anime Mahkemesi ───────────────────────────────────────────────────────────
Route::get('/mahkeme', [TribunalController::class, 'index'])->name('tribunal.index');
Route::get('/mahkeme/{tribunal}', [TribunalController::class, 'show'])->name('tribunal.show');
Route::get('/anime/{anime}/mahkeme', [TribunalController::class, 'forAnime'])->name('tribunal.for-anime');
Route::middleware('auth')->group(function () {
Route::post('/mahkeme', [TribunalController::class, 'store'])->name('tribunal.store');
Route::post('/mahkeme/{tribunal}/oy', [TribunalController::class, 'vote'])->name('tribunal.vote');
Route::post('/mahkeme/{tribunal}/arguman', [TribunalController::class, 'argue'])->name('tribunal.argue');
Route::post('/arguman/{argument}/oy', [TribunalController::class, 'argVote'])->name('tribunal.arg-vote');
});
// ── Zaman Kapsülü ─────────────────────────────────────────────────────────────
Route::middleware('auth')->group(function () {
Route::get('/kapsullerim', [SocialController::class, 'capsuleIndex'])->name('capsules.index');
Route::post('/kapsul', [SocialController::class, 'capsuleStore'])->name('capsule.store');
Route::post('/kapsul/{capsule}/ac', [SocialController::class, 'capsuleOpen'])->name('capsule.open');
});
// ── Spoiler Kilitli Kutu ──────────────────────────────────────────────────────
Route::get('/episode/{episode}/spoiler-boxes', [SocialController::class, 'spoilerBoxes'])->name('episode.spoiler-boxes');
Route::middleware('auth')->group(function () {
Route::post('/episode/{episode}/spoiler-boxes', [SocialController::class, 'spoilerBoxStore'])->name('episode.spoiler-box.store');
Route::post('/spoiler-box/{box}/like', [SocialController::class, 'spoilerBoxLike'])->name('spoiler-box.like');
});
// ── Ruh Hali Motoru ───────────────────────────────────────────────────────────
Route::post('/mood/recommend', [SocialController::class, 'moodRecommend'])->name('mood.recommend');
// ── Yasal Sayfalar ────────────────────────────────────────────────────────────
Route::get('/kullanim-kosullari', fn() => view('frontend.legal.terms'))->name('legal.terms');
Route::get('/gizlilik-politikasi', fn() => view('frontend.legal.privacy'))->name('legal.privacy');
Route::get('/telif-hakki', fn() => view('frontend.legal.dmca'))->name('legal.dmca');