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