Files
animexe/app/Http/Controllers/Admin/TrendingController.php
T
2026-07-14 00:01:48 +03:00

241 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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]);
}
}
}