Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,240 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\Episode;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TrendingController extends Controller
{
/**
* Trend yönetim sayfası.
*/
public function index()
{
$manual = Anime::where('is_trending', true)
->where('is_published', true)
->orderBy('trending_order')
->get();
$autoTrending = $this->getAutoTrending(20);
// Son skor hesaplama zamanı
$lastComputed = cache()->get('trending_score_computed_at');
return view('admin.trending.index', compact('manual', 'autoTrending', 'lastComputed'));
}
// ── Manuel trending yönetimi ──────────────────────────────────────────────
public function toggle(Request $request, Anime $anime)
{
$newState = !$anime->is_trending;
if ($newState) {
$maxOrder = Anime::where('is_trending', true)->max('trending_order') ?? 0;
$anime->update([
'is_trending' => true,
'trending_order' => $maxOrder + 1,
'trending_score' => $anime->trending_score + 200, // Manuel boost
]);
} else {
$anime->update(['is_trending' => false, 'trending_order' => 0]);
$this->reorderAll();
}
if ($request->wantsJson()) {
return response()->json(['ok' => true, 'is_trending' => $newState]);
}
return back()->with('success', $newState
? '"'.$anime->title.'" trend listesine eklendi.'
: '"'.$anime->title.'" trend listesinden çıkarıldı.');
}
public function reorder(Request $request)
{
$request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
foreach ($request->ids as $i => $id) {
Anime::where('id', $id)->update(['trending_order' => $i + 1]);
}
return response()->json(['ok' => true]);
}
public function move(Request $request, Anime $anime)
{
$direction = $request->input('direction');
$current = $anime->trending_order;
if ($direction === 'up' && $current > 1) {
$swap = Anime::where('is_trending', true)->where('trending_order', $current - 1)->first();
if ($swap) { $swap->update(['trending_order' => $current]); $anime->update(['trending_order' => $current - 1]); }
} elseif ($direction === 'down') {
$swap = Anime::where('is_trending', true)->where('trending_order', $current + 1)->first();
if ($swap) { $swap->update(['trending_order' => $current]); $anime->update(['trending_order' => $current + 1]); }
}
return back();
}
public function search(Request $request)
{
$results = Anime::where('is_published', true)
->where('title', 'like', "%{$request->query('q', '')}%")
->select('id', 'title', 'cover_image', 'is_trending', 'release_year', 'trending_score')
->take(8)->get()
->map(fn($a) => [
'id' => $a->id,
'title' => $a->title,
'cover' => $a->coverUrl,
'is_trending' => (bool) $a->is_trending,
'year' => $a->release_year,
'trending_score'=> round($a->trending_score, 1),
]);
return response()->json(['results' => $results]);
}
// ── Trend Skoru Hesaplama (YouTube algoritması) ───────────────────────────
/**
* Admin butonu: tüm animelerin trend skorunu hesapla ve kaydet.
* POST /admin/trending/compute-scores
*/
public function computeScores()
{
$count = self::runScoreComputation();
cache()->put('trending_score_computed_at', now()->toDateTimeString(), 3600);
cache()->flush(); // Anasayfa cache'ini temizle
return response()->json([
'ok' => true,
'updated' => $count,
'message' => "{$count} anime için trend skoru güncellendi.",
]);
}
/**
* YouTube-benzeri Trend Skoru Algoritması
* ─────────────────────────────────────────
* score = view_24h × 12 Son 24 saatin izlenme sayısı (en yüksek ağırlık)
* + view_7d × 4 Son 7 günün izlenme sayısı
* + view_30d × 1 Son 30 günün izlenme sayısı
* + watch_minutes_7d × 0.8 Gerçek izleme dakikası (kalite sinyali)
* + new_episode_bonus Yeni bölüm varsa büyük bonus
* + rating × 4 Kalite sinyali
* + manual_boost Manuel trending = +250
*
* Decay: Eski içeriklerin skoru doğal olarak düşer (view_count azalır).
* Herhangi bir yeni bölüm veya izlenme olmadan skor sıfıra yaklaşır.
*/
public static function runScoreComputation(): int
{
$now = now();
$day1 = $now->copy()->subDay();
$day7 = $now->copy()->subDays(7);
$day30 = $now->copy()->subDays(30);
$animes = DB::table('animes')
->where('is_published', true)
->select('id', 'rating', 'is_trending', 'status')
->get();
$updated = 0;
foreach ($animes as $anime) {
// ── Bölüm izlenme sayıları (view_count zaman dilimine göre) ──────
// Episode.updated_at → son izleme zamanının proxy'si
$views = DB::table('episodes')
->where('anime_id', $anime->id)
->where('is_published', true)
->selectRaw("
SUM(CASE WHEN updated_at >= ? THEN view_count ELSE 0 END) as v24h,
SUM(CASE WHEN updated_at >= ? THEN view_count ELSE 0 END) as v7d,
SUM(CASE WHEN updated_at >= ? THEN view_count ELSE 0 END) as v30d
", [$day1, $day7, $day30])
->first();
// ── Gerçek izleme dakikası (analytics_watch_events) ─────────────
$watchMinutes = 0;
try {
$watchMinutes = DB::table('analytics_watch_events')
->where('anime_id', $anime->id)
->where('created_at', '>=', $day7)
->sum('seconds_watched') / 60;
} catch (\Throwable) {}
// ── Yeni bölüm bonusu ────────────────────────────────────────────
$newEpBonus = 0;
$latestEpDate = DB::table('episodes')
->where('anime_id', $anime->id)
->where('is_published', true)
->max('created_at');
if ($latestEpDate) {
$epAge = now()->diffInHours($latestEpDate);
if ($epAge <= 24) $newEpBonus = 80; // Bugün yeni bölüm → çok büyük boost
elseif ($epAge <= 72) $newEpBonus = 40; // Son 3 gün
elseif ($epAge <= 168) $newEpBonus = 15; // Son 7 gün
elseif ($epAge <= 720) $newEpBonus = 5; // Son 30 gün
}
// ── Ongoing bonus ─────────────────────────────────────────────────
$ongoingBonus = ($anime->status === 'ongoing') ? 10 : 0;
// ── Manuel trending boost ─────────────────────────────────────────
$manualBoost = $anime->is_trending ? 250 : 0;
// ── Skor hesapla ─────────────────────────────────────────────────
$score =
($views->v24h ?? 0) * 12 +
($views->v7d ?? 0) * 4 +
($views->v30d ?? 0) * 1 +
$watchMinutes * 0.8 +
$newEpBonus +
$ongoingBonus +
((float)($anime->rating ?? 5)) * 4 +
$manualBoost;
DB::table('animes')
->where('id', $anime->id)
->update(['trending_score' => round($score, 2)]);
$updated++;
}
return $updated;
}
/**
* Auto-trending: trending_score'a göre sırala.
* Fallback: score kolonu yoksa eski yönteme dön.
*/
public static function getAutoTrending(int $limit = 12): \Illuminate\Support\Collection
{
try {
return Anime::where('is_published', true)
->orderByDesc('trending_score')
->take($limit)
->get();
} catch (\Throwable) {
// trending_score kolonu henüz oluşturulmamış → eski yöntem
return Anime::where('is_published', true)
->withSum(['episodes as recent_views' => fn($q) =>
$q->where('is_published', true)->where('updated_at', '>=', now()->subDays(30))
], 'view_count')
->orderByDesc('recent_views')
->take($limit)
->get();
}
}
private function reorderAll(): void
{
$animes = Anime::where('is_trending', true)->orderBy('trending_order')->get();
foreach ($animes as $i => $a) {
$a->update(['trending_order' => $i + 1]);
}
}
}