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');