241 lines
9.7 KiB
PHP
241 lines
9.7 KiB
PHP
<?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]);
|
||
}
|
||
}
|
||
}
|