Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+223
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\Api\ImportApiController;
|
||||
use App\Http\Controllers\Api\AuthApiController;
|
||||
use App\Http\Controllers\Api\AnimeApiController;
|
||||
use App\Http\Controllers\Api\UserApiController;
|
||||
use App\Http\Controllers\Api\CommentApiController;
|
||||
use App\Http\Controllers\Api\AiApiController;
|
||||
use App\Http\Controllers\Api\SocialApiController;
|
||||
use App\Http\Controllers\Api\TribunalApiController;
|
||||
use App\Http\Controllers\Api\MessageApiController;
|
||||
use App\Http\Controllers\Api\PlanApiController;
|
||||
|
||||
// ── Import API (internal Python scraper) ─────────────────────────────────────
|
||||
Route::middleware('import.api')->prefix('import')->group(function () {
|
||||
Route::get('test', [ImportApiController::class, 'test']);
|
||||
Route::get('stats', [ImportApiController::class, 'stats']);
|
||||
Route::get('settings', [ImportApiController::class, 'settings']);
|
||||
Route::get('next-job', [ImportApiController::class, 'nextJob']);
|
||||
Route::post('jobs', [ImportApiController::class, 'createJob']); // AUTO-IMPORT
|
||||
Route::get('imported-ids', [ImportApiController::class, 'importedIds']);
|
||||
Route::get('imported-titles', [ImportApiController::class, 'importedTitles']);
|
||||
Route::get('ongoing', [ImportApiController::class, 'ongoingAnimes']);
|
||||
Route::get('all-animes', [ImportApiController::class, 'allAniziumAnimes']);
|
||||
Route::get('jobs/{job}', [ImportApiController::class, 'getJob']);
|
||||
Route::post('jobs/{job}/status', [ImportApiController::class, 'updateStatus']);
|
||||
Route::post('jobs/{job}/episode', [ImportApiController::class, 'saveEpisode']);
|
||||
Route::post('jobs/{job}/subtitle', [ImportApiController::class, 'saveSubtitle']);
|
||||
Route::get('jobs/{job}/done-episodes', [ImportApiController::class, 'doneEpisodes']);
|
||||
|
||||
// Altyazı yenileme
|
||||
Route::get('anizium-episodes', [ImportApiController::class, 'aniziumEpisodes']);
|
||||
Route::post('subtitle-direct', [ImportApiController::class, 'saveSubtitleDirect']);
|
||||
|
||||
// Sağlık kontrolü + deaktivasyon
|
||||
Route::get('anizium-health', [ImportApiController::class, 'aniziumHealthData']);
|
||||
Route::post('deactivate-anime', [ImportApiController::class, 'deactivateAnime']);
|
||||
|
||||
// Animecix-specific
|
||||
Route::get('animecix/pending', [ImportApiController::class, 'animecixPendingJobs']);
|
||||
Route::post('episodes/{episode}/video-sources', [ImportApiController::class, 'saveVideoSources']);
|
||||
|
||||
// Çapraz kaynak desteği
|
||||
Route::get('anime/lookup', [ImportApiController::class, 'animeLookup']);
|
||||
Route::get('published-animes-with-jobs', [ImportApiController::class, 'publishedAnimesWithJobs']);
|
||||
});
|
||||
|
||||
// ── Mobile App API ────────────────────────────────────────────────────────────
|
||||
|
||||
// App status (public — called on startup to check maintenance/force-update)
|
||||
Route::get('app-status', function () {
|
||||
$minVersion = \App\Models\Setting::get('mobile_min_version', '1.0.0');
|
||||
$currentVersion = \App\Models\Setting::get('mobile_current_version', '1.0.0');
|
||||
$maintenance = \App\Models\Setting::get('mobile_maintenance_mode', '0') === '1';
|
||||
$maintMsg = \App\Models\Setting::get('mobile_maintenance_message', 'Uygulama şu anda bakımda.');
|
||||
$apkUrl = \App\Models\Setting::get('mobile_apk_url', '');
|
||||
$forceMsg = \App\Models\Setting::get('mobile_force_update_msg', 'Lütfen uygulamayı güncelleyin.');
|
||||
|
||||
// Determine force_update by comparing appVersion (sent as ?v=x.y.z) with minVersion
|
||||
$appVersion = request('v', '0.0.0');
|
||||
$forceUpdate = version_compare($appVersion, $minVersion, '<');
|
||||
|
||||
return response()->json([
|
||||
'maintenance' => $maintenance,
|
||||
'maintenance_message' => $maintMsg,
|
||||
'min_version' => $minVersion,
|
||||
'current_version' => $currentVersion,
|
||||
'apk_url' => $apkUrl ?: null,
|
||||
'force_update' => $forceUpdate,
|
||||
'force_update_message'=> $forceMsg,
|
||||
]);
|
||||
});
|
||||
|
||||
// Auth (public)
|
||||
Route::prefix('auth')->group(function () {
|
||||
Route::post('register', [AuthApiController::class, 'register']);
|
||||
Route::post('login', [AuthApiController::class, 'login']);
|
||||
});
|
||||
|
||||
// Public content
|
||||
Route::get('home', [AnimeApiController::class, 'home']);
|
||||
Route::get('animes', [AnimeApiController::class, 'index']);
|
||||
Route::get('animes/{slug}', [AnimeApiController::class, 'show']);
|
||||
Route::get('genres', [AnimeApiController::class, 'genres']);
|
||||
Route::get('genres/{slug}', [AnimeApiController::class, 'genre']);
|
||||
Route::get('watch/{slug}/{season}/{episode}', [AnimeApiController::class, 'watch']);
|
||||
|
||||
// AniSkip (public — no auth needed, does Jikan+AniSkip lookup with caching)
|
||||
Route::get('aniskip/{slug}/{season}/{episode}', [AnimeApiController::class, 'aniSkip']);
|
||||
|
||||
// Skip segments (public — no auth needed)
|
||||
Route::get('episodes/{episode}/skip-segments', [AnimeApiController::class, 'skipSegments']);
|
||||
Route::post('episodes/{episode}/skip-event', [AnimeApiController::class, 'recordSkipEvent'])
|
||||
->middleware('throttle:60,1'); // max 60 events per minute per IP
|
||||
|
||||
// HEVC flag — player tarafından HEVC hatası alınan kaynakları işaretler
|
||||
Route::post('sources/flag-hevc', [AnimeApiController::class, 'flagHevc'])
|
||||
->middleware('throttle:30,1');
|
||||
|
||||
// Reklam istatistikleri — path bilerek nötr ('ads' değil), adblocker /api/ads/ engelliyor
|
||||
Route::post('slot/{ad}/view', [\App\Http\Controllers\Api\AdApiController::class, 'impression'])
|
||||
->middleware('throttle:30,1');
|
||||
Route::post('slot/{ad}/hit', [\App\Http\Controllers\Api\AdApiController::class, 'click'])
|
||||
->middleware('throttle:30,1');
|
||||
|
||||
// Comments (public read, auth write)
|
||||
Route::get('comments', [CommentApiController::class, 'index']);
|
||||
|
||||
// NicoNico timestamp comments (public read)
|
||||
Route::get('episodes/{episode}/nico-comments', [SocialApiController::class, 'timestampComments']);
|
||||
|
||||
// Predictions (public read)
|
||||
Route::get('episodes/{episode}/predictions', [SocialApiController::class, 'predictions']);
|
||||
|
||||
// Spoiler boxes (public read)
|
||||
Route::get('episodes/{episode}/spoiler-boxes', [SocialApiController::class, 'spoilerBoxes']);
|
||||
|
||||
// Tribunal (public read)
|
||||
Route::get('tribunals', [TribunalApiController::class, 'index']);
|
||||
Route::get('tribunals/{tribunal}', [TribunalApiController::class, 'show']);
|
||||
|
||||
// Watch party info (public)
|
||||
Route::get('party/{roomCode}', [SocialApiController::class, 'partyInfo']);
|
||||
|
||||
// Mood engine (public)
|
||||
Route::post('mood/recommend', [SocialApiController::class, 'moodRecommend']);
|
||||
|
||||
// Anime requests (public list)
|
||||
Route::get('anime-request', [UserApiController::class, 'requestIndex']);
|
||||
|
||||
// Notifications count (optional auth)
|
||||
Route::get('notifications/count', [UserApiController::class, 'notificationsCount'])
|
||||
->middleware('auth:sanctum');
|
||||
|
||||
// ── Auth required ─────────────────────────────────────────────────────────────
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
// Auth
|
||||
Route::post('auth/logout', [AuthApiController::class, 'logout']);
|
||||
Route::get('auth/me', [AuthApiController::class, 'me']);
|
||||
Route::post('auth/profile', [AuthApiController::class, 'updateProfile']);
|
||||
Route::post('auth/fcm-token', [AuthApiController::class, 'saveFcmToken']);
|
||||
|
||||
// Profile stats
|
||||
Route::get('profile/stats', [UserApiController::class, 'profileStats']);
|
||||
|
||||
// Watchlist
|
||||
Route::get('watchlist', [UserApiController::class, 'watchlist']);
|
||||
Route::post('watchlist/{anime}/toggle', [UserApiController::class, 'watchlistToggle']);
|
||||
|
||||
// Continue watching
|
||||
Route::post('continue-watching', [UserApiController::class, 'continueWatchingUpdate']);
|
||||
|
||||
// Rate & Follow
|
||||
Route::post('anime/{anime}/rate', [UserApiController::class, 'animeRate']);
|
||||
Route::post('anime/{anime:slug}/follow', [UserApiController::class, 'followToggle']);
|
||||
|
||||
// Notifications
|
||||
Route::get('notifications', [UserApiController::class, 'notifications']);
|
||||
|
||||
// Achievements
|
||||
Route::get('achievements', [UserApiController::class, 'achievements']);
|
||||
|
||||
// Episode notes
|
||||
Route::post('episode/{episode}/note', [UserApiController::class, 'noteStore']);
|
||||
Route::delete('notes/{note}', [UserApiController::class, 'noteDelete']);
|
||||
Route::get('episode/{episode}/notes', [UserApiController::class, 'episodeNotesList']);
|
||||
|
||||
// Comments
|
||||
Route::post('comments', [CommentApiController::class, 'store']);
|
||||
Route::post('comments/{comment}/like', [CommentApiController::class, 'like']);
|
||||
|
||||
// Anime requests
|
||||
Route::post('anime-request', [UserApiController::class, 'requestStore']);
|
||||
Route::post('anime-request/{animeRequest}/vote', [UserApiController::class, 'requestVote']);
|
||||
|
||||
// AI
|
||||
Route::post('ai/chat', [AiApiController::class, 'chat']);
|
||||
Route::post('ai/recommend', [AiApiController::class, 'recommend']);
|
||||
Route::post('ai/similar', [AiApiController::class, 'similar']);
|
||||
Route::post('ai/search', [AiApiController::class, 'search']);
|
||||
|
||||
// NicoNico (auth write)
|
||||
Route::post('episodes/{episode}/nico-comments', [SocialApiController::class, 'timestampCommentStore']);
|
||||
|
||||
// Predictions (auth write)
|
||||
Route::post('episodes/{episode}/predictions', [SocialApiController::class, 'predictionStore']);
|
||||
Route::post('predictions/{prediction}/vote', [SocialApiController::class, 'predictionVote']);
|
||||
|
||||
// Spoiler boxes (auth write)
|
||||
Route::post('episodes/{episode}/spoiler-boxes', [SocialApiController::class, 'spoilerBoxStore']);
|
||||
Route::post('spoiler-boxes/{box}/like', [SocialApiController::class, 'spoilerBoxLike']);
|
||||
|
||||
// Time capsule
|
||||
Route::get('capsules', [SocialApiController::class, 'capsuleIndex']);
|
||||
Route::post('capsules', [SocialApiController::class, 'capsuleStore']);
|
||||
Route::post('capsules/{capsule}/open', [SocialApiController::class, 'capsuleOpen']);
|
||||
|
||||
// Tribunal (auth write)
|
||||
Route::post('tribunals', [TribunalApiController::class, 'store']);
|
||||
Route::post('tribunals/{tribunal}/vote', [TribunalApiController::class, 'vote']);
|
||||
Route::post('tribunals/{tribunal}/argue', [TribunalApiController::class, 'argue']);
|
||||
Route::post('tribunal-arguments/{argument}/vote', [TribunalApiController::class, 'argVote']);
|
||||
|
||||
// Watch party
|
||||
Route::post('party/create', [SocialApiController::class, 'partyCreate']);
|
||||
Route::post('party/{roomCode}/join', [SocialApiController::class, 'partyJoin']);
|
||||
Route::post('party/{roomCode}/sync', [SocialApiController::class, 'partySync']);
|
||||
Route::post('party/{roomCode}/leave', [SocialApiController::class, 'partyLeave']);
|
||||
|
||||
// User follow
|
||||
Route::post('users/{user}/follow', [SocialApiController::class, 'followToggle']);
|
||||
|
||||
// Plans
|
||||
Route::get('plans', [PlanApiController::class, 'index']);
|
||||
|
||||
// ── Mesajlar (Mobile Chat) — sabit rotalar önce gelmeli
|
||||
Route::get('messages', [MessageApiController::class, 'conversations']);
|
||||
Route::post('messages/start/{user}', [MessageApiController::class, 'startConversation']);
|
||||
Route::get('messages/{conversation}', [MessageApiController::class, 'show']);
|
||||
Route::post('messages/{conversation}', [MessageApiController::class, 'send']);
|
||||
Route::get('messages/{conversation}/poll', [MessageApiController::class, 'poll']);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
// AI blog: günde 3 kez (09:00, 15:00, 21:00) — her seferinde 1 yeni yazı
|
||||
Schedule::command('animexe:generate-blogs --count=1')
|
||||
->cron('0 9,15,21 * * *')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// AI anime meta doldurma: her gece 02:00 — eksik tüm animeleri işle, bititiğinde dur
|
||||
Schedule::command('animexe:fill-anime-meta')
|
||||
->dailyAt('02:00')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
// Her saat başı süresi dolan mahkemeleri kapat
|
||||
Schedule::command('tribunals:close')
|
||||
->hourly()
|
||||
->withoutOverlapping();
|
||||
|
||||
// Her gece 02:30 — kapak/banneri eksik animelere AniList'ten resim çek
|
||||
Schedule::command('anime:fetch-images --missing --limit=100')
|
||||
->dailyAt('02:30')
|
||||
->withoutOverlapping()
|
||||
->runInBackground();
|
||||
|
||||
+742
@@ -0,0 +1,742 @@
|
||||
<?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;
|
||||
|
||||
Route::get('/media/{path}', [MediaController::class, 'show'])
|
||||
->where('path', '.*')
|
||||
->name('media.show');
|
||||
|
||||
// ── 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);
|
||||
|
||||
// Block private/internal IPs (SSRF protection)
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
if (!$host || preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) abort(403);
|
||||
// Only allow HTTPS streaming URLs (m3u8 or ts segments)
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
||||
if (!in_array($scheme, ['http', 'https'])) abort(403);
|
||||
|
||||
// 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
|
||||
$fetchUpstream = function (string $url, array $headers) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$ctype = (string) curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
$errNo = curl_errno($ch);
|
||||
$errStr = $errNo ? curl_strerror($errNo) : '';
|
||||
curl_close($ch);
|
||||
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);
|
||||
|
||||
$host = parse_url($url, PHP_URL_HOST) ?? "";
|
||||
if (!$host || preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/', $host)) abort(403);
|
||||
$scheme = parse_url($url, PHP_URL_SCHEME) ?? "";
|
||||
if (!in_array($scheme, ['http', 'https'])) abort(403);
|
||||
|
||||
// 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
|
||||
$segFetch = function (string $url, array $headers) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
]);
|
||||
$content = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
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;
|
||||
foreach ($allowed as $a) { if (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');
|
||||
Reference in New Issue
Block a user