takeone-youtube-clone/app/Http/Controllers/SuperAdminController.php
ghassan 0b2e95ea65 Add NAS file manager integration and all pending platform changes
- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
  settings panel, country/phone/timezone components, posts, slideshow,
  playlist shares, video downloads/shares, comment likes, notifications,
  social links, and more

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:24:32 +03:00

898 lines
39 KiB
PHP
Raw Permalink 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;
use App\Models\AuditLog;
use App\Models\Setting;
use App\Models\User;
use App\Models\Video;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class SuperAdminController extends Controller
{
public function __construct()
{
$this->middleware('super_admin')->except(['exitImpersonation']);
}
// Manually verify a user's email
public function verifyUser(User $user)
{
if ($user->email_verified_at) {
return back()->with('error', "{$user->name} is already verified.");
}
$user->email_verified_at = now();
$user->save();
return back()->with('success', "{$user->name}'s account has been verified.");
}
// Start impersonating a user
public function impersonate(User $user)
{
if ($user->isSuperAdmin()) {
return back()->with('error', 'You cannot impersonate another super admin.');
}
$admin = \Auth::user();
AuditLog::record('admin.impersonate', [
'user_id' => $admin->id,
'user_name' => $admin->name,
'subject_type' => 'User',
'subject_id' => (string) $user->id,
'subject_label' => $user->name,
]);
session(['impersonator_id' => \Auth::id()]);
\Auth::loginUsingId($user->id);
return redirect()->route('home')
->with('success', "You are now impersonating {$user->name}. Use the banner to exit.");
}
// Exit impersonation and return to original admin account
public function exitImpersonation()
{
$impersonatorId = session('impersonator_id');
if (! $impersonatorId) {
return redirect()->route('home');
}
$impersonatedUser = \Auth::user();
session()->forget('impersonator_id');
\Auth::loginUsingId($impersonatorId);
AuditLog::record('admin.impersonate.exit', [
'subject_type' => 'User',
'subject_id' => (string) $impersonatedUser->id,
'subject_label' => $impersonatedUser->name,
]);
return redirect()->route('admin.users')
->with('success', 'Impersonation ended. You are back as yourself.');
}
// Dashboard - Overview statistics
public function dashboard()
{
$now = now();
$w0 = $now->copy()->subDays(7); // start of this week window
$w1 = $now->copy()->subDays(14); // start of last week window
// ── Core totals ────────────────────────────────────────────
$totalUsers = User::count();
$totalVideos = Video::count();
$totalViews = \DB::table('video_views')->count();
$totalLikes = \DB::table('video_likes')->count();
$totalComments = \DB::table('comments')->count();
// ── Week-over-week growth ──────────────────────────────────
$usersThisWeek = User::where('created_at', '>=', $w0)->count();
$usersLastWeek = User::whereBetween('created_at', [$w1, $w0])->count();
$videosThisWeek = Video::where('created_at', '>=', $w0)->count();
$videosLastWeek = Video::whereBetween('created_at', [$w1, $w0])->count();
$viewsThisWeek = \DB::table('video_views')->where('watched_at', '>=', $w0)->count();
$viewsLastWeek = \DB::table('video_views')->whereBetween('watched_at', [$w1, $w0])->count();
$likesThisWeek = \DB::table('video_likes')->where('created_at', '>=', $w0)->count();
$likesLastWeek = \DB::table('video_likes')->whereBetween('created_at', [$w1, $w0])->count();
$commentsThisWeek = \DB::table('comments')->where('created_at', '>=', $w0)->count();
$growthUsers = $this->growthPct($usersLastWeek, $usersThisWeek);
$growthVideos = $this->growthPct($videosLastWeek, $videosThisWeek);
$growthViews = $this->growthPct($viewsLastWeek, $viewsThisWeek);
$growthLikes = $this->growthPct($likesLastWeek, $likesThisWeek);
$stats = compact(
'totalUsers','totalVideos','totalViews','totalLikes','totalComments',
'usersThisWeek','videosThisWeek','viewsThisWeek','likesThisWeek','commentsThisWeek',
'growthUsers','growthVideos','growthViews','growthLikes'
);
// ── 30-day daily activity (for charts) ────────────────────
$days30 = collect(range(29, 0))->map(fn($d) => $now->copy()->subDays($d)->format('Y-m-d'));
$rawUsers = User::selectRaw('DATE(created_at) as d, COUNT(*) as n')
->where('created_at', '>=', $now->copy()->subDays(30))
->groupBy('d')->pluck('n', 'd');
$rawVideos = Video::selectRaw('DATE(created_at) as d, COUNT(*) as n')
->where('created_at', '>=', $now->copy()->subDays(30))
->groupBy('d')->pluck('n', 'd');
$rawViews = \DB::table('video_views')
->selectRaw('DATE(watched_at) as d, COUNT(*) as n')
->where('watched_at', '>=', $now->copy()->subDays(30))
->groupBy('d')->pluck('n', 'd');
$chartLabels = $days30->map(fn($d) => date('M j', strtotime($d)))->values()->toJson();
$chartDatesRaw = $days30->values()->toJson();
$chartUsersData = $days30->map(fn($d) => $rawUsers->get($d, 0))->values()->toJson();
$chartVideosData = $days30->map(fn($d) => $rawVideos->get($d, 0))->values()->toJson();
$chartViewsData = $days30->map(fn($d) => $rawViews->get($d, 0))->values()->toJson();
// ── Video status & visibility ──────────────────────────────
$videosByStatus = Video::selectRaw('status, count(*) as n')->groupBy('status')->pluck('n', 'status');
$videosByVisibility = Video::selectRaw('visibility, count(*) as n')->groupBy('visibility')->pluck('n', 'visibility');
$videosByType = Video::selectRaw('type, count(*) as n')->groupBy('type')->pluck('n', 'type');
// ── Alerts ────────────────────────────────────────────────
$failedCount = $videosByStatus->get('failed', 0);
$processingCount = $videosByStatus->get('processing', 0);
$pendingCount = $videosByStatus->get('pending', 0);
// ── Top content ───────────────────────────────────────────
$topVideos = \DB::table('videos')
->join('users', 'videos.user_id', '=', 'users.id')
->select(
'videos.id', 'videos.title', 'videos.thumbnail', 'videos.type',
'users.name as username',
\DB::raw('(SELECT COUNT(*) FROM video_views WHERE video_views.video_id = videos.id) as view_count'),
\DB::raw('(SELECT COUNT(*) FROM video_likes WHERE video_likes.video_id = videos.id) as like_count')
)
->where('videos.status', 'ready')
->orderByDesc('view_count')
->take(5)->get();
$topUploaders = \DB::table('users')
->select('users.id','users.name','users.email','users.avatar',
\DB::raw('COUNT(videos.id) as video_count'),
\DB::raw('SUM((SELECT COUNT(*) FROM video_views WHERE video_views.video_id = videos.id)) as total_views')
)
->leftJoin('videos', 'users.id', '=', 'videos.user_id')
->groupBy('users.id','users.name','users.email','users.avatar')
->orderByDesc('video_count')
->take(5)->get();
// ── Engagement ────────────────────────────────────────────
$readyVideos = $videosByStatus->get('ready', 0);
$avgViewsPerVideo = $readyVideos > 0 ? round($totalViews / $readyVideos, 1) : 0;
$likeToViewRatio = $totalViews > 0 ? round(($totalLikes / $totalViews) * 100, 1) : 0;
$avgVideosPerUser = $totalUsers > 0 ? round($totalVideos / $totalUsers, 1) : 0;
// ── Viewers by country ────────────────────────────────────
$viewsByCountry = \DB::table('video_views')
->whereNotNull('country')
->selectRaw('country, country_name, COUNT(*) as total')
->groupBy('country', 'country_name')
->orderByDesc('total')
->take(20)
->get();
// ── Recent ────────────────────────────────────────────────
$recentUsers = User::latest()->take(5)->get();
$recentVideos = Video::with('user')->latest()->take(5)->get();
// ── Storage ───────────────────────────────────────────────
$disk = Storage::disk('public');
$sizeVideos = $this->dirSize($disk, 'videos');
$sizeThumbnails = $this->dirSize($disk, 'thumbnails');
$sizeAvatars = $this->dirSize($disk, 'avatars');
$sizeImages = $this->dirSize($disk, 'images');
$totalPublicSize = $sizeVideos + $sizeThumbnails + $sizeAvatars + $sizeImages;
$videosUsagePercent = $totalPublicSize > 0 ? round(($sizeVideos / $totalPublicSize) * 100, 1) : 0;
// Convert to MB
$toMb = fn($b) => round($b / 1024 / 1024, 1);
$storage = [
'videos' => $toMb($sizeVideos),
'thumbnails' => $toMb($sizeThumbnails),
'avatars' => $toMb($sizeAvatars),
'images' => $toMb($sizeImages),
'total' => $toMb($totalPublicSize),
];
$videosDirSize = $storage['videos'];
$totalPublicSizeMb = $storage['total'];
return view('admin.dashboard', compact(
'stats',
'chartLabels','chartDatesRaw','chartUsersData','chartVideosData','chartViewsData',
'videosByStatus','videosByVisibility','videosByType',
'failedCount','processingCount','pendingCount',
'topVideos','topUploaders',
'avgViewsPerVideo','likeToViewRatio','avgVideosPerUser',
'recentUsers','recentVideos',
'storage','videosDirSize','totalPublicSizeMb','videosUsagePercent',
'viewsByCountry'
));
}
private function growthPct(int $prev, int $curr): array
{
if ($prev === 0) {
return ['pct' => $curr > 0 ? 100 : 0, 'dir' => $curr > 0 ? 'up' : 'flat'];
}
$pct = round((($curr - $prev) / $prev) * 100, 1);
return ['pct' => abs($pct), 'dir' => $pct >= 0 ? 'up' : 'down'];
}
private function dirSize($disk, string $dir): int
{
$size = 0;
foreach ($disk->allFiles($dir) as $file) {
try { $size += $disk->size($file); } catch (\Exception $e) {}
}
return $size;
}
// List all users with search/filter
public function users(Request $request)
{
$query = User::query();
// Search by name or email
if ($request->has('search') && $request->search) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('email', 'like', "%{$search}%");
});
}
// Filter by role
if ($request->has('role') && $request->role) {
$query->where('role', $request->role);
}
// Sort by
$sort = $request->get('sort', 'latest');
switch ($sort) {
case 'oldest':
$query->oldest();
break;
case 'name_asc':
$query->orderBy('name', 'asc');
break;
case 'name_desc':
$query->orderBy('name', 'desc');
break;
default:
$query->latest();
}
$users = $query->paginate(20);
$users->appends($request->query());
return view('admin.users', compact('users'));
}
// Show edit user form
public function editUser(User $user)
{
return view('admin.edit-user', compact('user'));
}
// Update user
public function updateUser(Request $request, User $user)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
'role' => 'required|in:user,admin,super_admin',
'new_password' => 'nullable|min:8|confirmed',
]);
$data = [
'name' => $request->name,
'email' => $request->email,
'role' => $request->role,
];
// Update password if provided
if ($request->new_password) {
$data['password'] = Hash::make($request->new_password);
}
$user->update($data);
return redirect()->route('admin.users')->with('success', 'User updated successfully!');
}
// Delete user
public function deleteUser(User $user)
{
// Prevent deleting yourself
if (auth()->id() === $user->id) {
return back()->with('error', 'You cannot delete your own account!');
}
AuditLog::record('admin.user.deleted', [
'subject_type' => 'User',
'subject_id' => (string) $user->id,
'subject_label' => $user->name,
'details' => ['email' => $user->email, 'video_count' => $user->videos->count()],
]);
// Delete user's videos and associated files
foreach ($user->videos as $video) {
Storage::delete('public/videos/' . $video->filename);
if ($video->thumbnail) {
Storage::delete('public/thumbnails/' . $video->thumbnail);
}
}
$user->videos()->delete();
// Delete user likes and views - use direct query since relationship is named 'viewers'
\DB::table('video_likes')->where('user_id', $user->id)->delete();
\DB::table('video_views')->where('user_id', $user->id)->delete();
$user->delete();
return redirect()->route('admin.users')->with('success', 'User deleted successfully!');
}
// List all videos
public function videos(Request $request)
{
$query = Video::with('user');
// Search by title or description
if ($request->has('search') && $request->search) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
// Filter by status
if ($request->has('status') && $request->status) {
$query->where('status', $request->status);
}
// Filter by visibility
if ($request->has('visibility') && $request->visibility) {
$query->where('visibility', $request->visibility);
}
// Filter by type
if ($request->has('type') && $request->type) {
$query->where('type', $request->type);
}
// Sort by
$sort = $request->get('sort', 'latest');
switch ($sort) {
case 'oldest':
$query->oldest();
break;
case 'title_asc':
$query->orderBy('title', 'asc');
break;
case 'title_desc':
$query->orderBy('title', 'desc');
break;
case 'views':
// Can't use withCount for views due to pivot table issue
$query->latest();
break;
case 'likes':
$query->withCount('likes')->orderBy('likes_count', 'desc');
break;
default:
$query->latest();
}
$videos = $query->paginate(20);
$videos->appends($request->query());
return view('admin.videos', compact('videos'));
}
// Per-video analytics
public function videoAnalytics(Video $video)
{
// ── Total view events (every watch counts) ──────────────────────────
$totalViews = \DB::table('video_views')->where('video_id', $video->id)->count();
$totalLikes = \DB::table('video_likes')->where('video_id', $video->id)->count();
$totalComments = \DB::table('comments')->where('video_id', $video->id)->count();
// ── Views per day last 30 days (raw events) ───────────────────────
$rawDaily = \DB::table('video_views')
->where('video_id', $video->id)
->where('watched_at', '>=', now()->subDays(29)->startOfDay())
->selectRaw('DATE(watched_at) as date, COUNT(*) as total')
->groupBy('date')
->orderBy('date')
->get()
->keyBy('date');
$dailyLabels = [];
$dailyViews = [];
for ($i = 29; $i >= 0; $i--) {
$d = now()->subDays($i);
$dailyLabels[] = $d->format('M d');
$dailyViews[] = $rawDaily[$d->format('Y-m-d')]->total ?? 0;
}
// ── Fetch all view records once, join user profile data ─────────────
// Ordered newest-first so the first occurrence per viewer is their
// most recent record (country, etc. from their latest watch).
$allViewRecords = \DB::table('video_views')
->leftJoin('users', 'video_views.user_id', '=', 'users.id')
->where('video_views.video_id', $video->id)
->select(
'video_views.id',
'video_views.user_id',
'video_views.ip_address',
'video_views.country',
'video_views.country_name',
'video_views.watched_at',
'users.name as viewer_name',
'users.avatar as viewer_avatar',
'users.birthday',
'users.gender'
)
->orderByDesc('video_views.watched_at')
->get();
// ── Deduplicate to one row per unique viewer ─────────────────────────
// Auth users → keyed by user_id (u_123)
// Guest users → keyed by ip_address (i_1.2.3.4)
$seenKeys = [];
$uniqueViewers = [];
foreach ($allViewRecords as $row) {
$key = $row->user_id ? 'u_' . $row->user_id : 'i_' . $row->ip_address;
if (isset($seenKeys[$key])) continue;
$seenKeys[$key] = true;
$uniqueViewers[] = $row;
}
$totalUniqueViewers = count($uniqueViewers);
$authViewers = count(array_filter($uniqueViewers, fn($v) => $v->user_id !== null));
$guestViewers = $totalUniqueViewers - $authViewers;
// ── Countries one count per unique viewer ──────────────────────────
$countryMap = [];
foreach ($uniqueViewers as $viewer) {
if (!$viewer->country) continue;
if (!isset($countryMap[$viewer->country])) {
$countryMap[$viewer->country] = ['country' => $viewer->country, 'country_name' => $viewer->country_name, 'total' => 0];
}
$countryMap[$viewer->country]['total']++;
}
usort($countryMap, fn($a, $b) => $b['total'] - $a['total']);
$viewsByCountry = collect(array_slice($countryMap, 0, 20))->map(fn($r) => (object) $r);
// ── Age groups one count per unique viewer ─────────────────────────
$ageGroups = ['Under 18' => 0, '1824' => 0, '2534' => 0, '3544' => 0, '4554' => 0, '55+' => 0, 'Unknown' => 0];
$now = now();
foreach ($uniqueViewers as $viewer) {
if (!$viewer->birthday) { $ageGroups['Unknown']++; continue; }
$age = \Carbon\Carbon::parse($viewer->birthday)->diffInYears($now);
if ($age < 18) $ageGroups['Under 18']++;
elseif ($age < 25) $ageGroups['1824']++;
elseif ($age < 35) $ageGroups['2534']++;
elseif ($age < 45) $ageGroups['3544']++;
elseif ($age < 55) $ageGroups['4554']++;
else $ageGroups['55+']++;
}
// ── Gender one count per unique viewer ─────────────────────────────
$genderCounts = ['Male' => 0, 'Female' => 0, 'Prefer not to say' => 0];
foreach ($uniqueViewers as $viewer) {
if ($viewer->gender === 'male') $genderCounts['Male']++;
elseif ($viewer->gender === 'female') $genderCounts['Female']++;
elseif ($viewer->gender === 'prefer_not_to_say') $genderCounts['Prefer not to say']++;
}
// ── Recent 20 individual view events ────────────────────────────────
$recentViews = $allViewRecords->take(20);
return view('admin.video-analytics', compact(
'video', 'totalViews', 'totalUniqueViewers', 'authViewers', 'guestViewers',
'totalLikes', 'totalComments', 'dailyLabels', 'dailyViews',
'viewsByCountry', 'ageGroups', 'genderCounts', 'recentViews'
));
}
// Show edit video form
public function editVideo(Video $video)
{
return view('admin.edit-video', compact('video'));
}
// Update video
public function updateVideo(Request $request, Video $video)
{
$request->validate([
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'visibility' => 'required|in:public,unlisted,private',
'type' => 'required|in:generic,music,match',
'status' => 'required|in:pending,processing,ready,failed',
'is_shorts' => 'nullable|boolean',
'download_access' => 'nullable|in:disabled,everyone,registered,subscribers',
]);
$data = $request->only(['title', 'description', 'visibility', 'type', 'status', 'is_shorts', 'download_access']);
$video->update($data);
return redirect()->route('admin.videos')->with('success', 'Video updated successfully!');
}
// Delete video
public function deleteVideo(Video $video)
{
$videoTitle = $video->title;
AuditLog::record('admin.video.deleted', [
'subject_type' => 'Video',
'subject_id' => (string) $video->id,
'subject_label' => $videoTitle,
'details' => ['owner_id' => $video->user_id, 'type' => $video->type],
]);
// Delete files
Storage::delete('public/videos/' . $video->filename);
if ($video->thumbnail) {
Storage::delete('public/thumbnails/' . $video->thumbnail);
}
// Delete likes and views - use direct queries since relationships have timestamp issues
\DB::table('video_likes')->where('video_id', $video->id)->delete();
\DB::table('video_views')->where('video_id', $video->id)->delete();
$video->delete();
return redirect()->route('admin.videos')->with('success', 'Video "' . $videoTitle . '" deleted successfully!');
}
/**
* Manual cleanup orphaned videos (admin instant trigger)
*/
public function cleanupOrphanedVideos(Request $request)
{
$output = [];
$returnCode = 0;
Artisan::call('cleanup:orphaned-videos --force', [], $output);
Log::channel('orphaned-videos')->info('Manual cleanup triggered by super admin');
$disk = Storage::disk('public');
$totalPublicSize = 0;
foreach ($disk->allFiles() as $file) {
$totalPublicSize += $disk->size($file);
}
$videosDirSize = 0;
foreach ($disk->allFiles('videos') as $file) {
$videosDirSize += $disk->size($file);
}
return response()->json([
'success' => true,
'message' => 'Cleanup completed! Check logs for details.',
'stats' => [
'return_code' => $returnCode,
'videos_dir_size_mb' => round($videosDirSize / 1024 / 1024, 1),
'total_public_size_mb' => round($totalPublicSize / 1024 / 1024, 1),
],
]);
}
public function modalData(Request $request)
{
$resource = $request->get('resource', '');
$limit = min((int) $request->get('limit', 30), 100);
switch ($resource) {
case 'users':
$query = User::latest();
if ($request->date) $query->whereDate('created_at', $request->date);
if ($request->filter === 'week') $query->where('created_at', '>=', now()->subDays(7));
$items = $query->take($limit)->get();
return response()->json(['type' => 'users', 'items' => $items->map(fn($u) => [
'avatar' => $u->avatar_url,
'name' => $u->name,
'email' => $u->email,
'role' => $u->role ?? 'user',
'joined' => $u->created_at->diffForHumans(),
'url' => route('channel', $u->channel),
])]);
case 'videos':
$query = Video::with('user')->latest();
if ($request->status) $query->where('status', $request->status);
if ($request->visibility) $query->where('visibility', $request->visibility);
if ($request->type) $query->where('type', $request->type);
if ($request->date) $query->whereDate('created_at', $request->date);
if ($request->uploader_id) $query->where('user_id', $request->uploader_id);
$items = $query->take($limit)->get();
return response()->json(['type' => 'videos', 'items' => $items->map(fn($v) => [
'thumbnail' => $v->thumbnail ? asset('storage/thumbnails/'.$v->thumbnail) : null,
'title' => $v->title,
'owner' => $v->user->name ?? 'Unknown',
'status' => $v->status,
'type' => $v->type,
'views' => \DB::table('video_views')->where('video_id', $v->id)->count(),
'uploaded' => $v->created_at->diffForHumans(),
'url' => route('videos.show', $v),
])]);
case 'top_videos':
$items = \DB::table('videos')
->join('users', 'videos.user_id', '=', 'users.id')
->select('videos.id', 'videos.title', 'videos.thumbnail', 'users.name as username',
\DB::raw('(SELECT COUNT(*) FROM video_views WHERE video_views.video_id = videos.id) as view_count'),
\DB::raw('(SELECT COUNT(*) FROM video_likes WHERE video_likes.video_id = videos.id) as like_count'))
->orderByDesc('view_count')->take($limit)->get();
return response()->json(['type' => 'top_videos', 'items' => $items->map(fn($v) => [
'thumbnail' => $v->thumbnail ? asset('storage/thumbnails/'.$v->thumbnail) : null,
'title' => $v->title,
'owner' => $v->username,
'views' => $v->view_count,
'likes' => $v->like_count,
'url' => route('videos.show', Video::encodeId($v->id)),
])]);
case 'views_day':
$items = \DB::table('video_views')
->join('videos', 'video_views.video_id', '=', 'videos.id')
->leftJoin('users', 'video_views.user_id', '=', 'users.id')
->select('videos.title', 'videos.id as video_id', 'users.name as viewer_name', 'video_views.country', 'video_views.watched_at')
->when($request->date, fn($q) => $q->whereDate('video_views.watched_at', $request->date))
->orderByDesc('video_views.watched_at')->take($limit)->get();
return response()->json(['type' => 'views', 'items' => $items->map(fn($v) => [
'video' => $v->title,
'viewer' => $v->viewer_name ?? 'Guest',
'country' => $v->country ?? '—',
'time' => \Carbon\Carbon::parse($v->watched_at)->diffForHumans(),
'url' => route('videos.show', Video::encodeId($v->video_id)),
])]);
case 'likes':
$items = \DB::table('videos')
->join('users', 'videos.user_id', '=', 'users.id')
->select('videos.id', 'videos.title', 'videos.thumbnail', 'users.name as username',
\DB::raw('(SELECT COUNT(*) FROM video_likes WHERE video_likes.video_id = videos.id) as like_count'))
->having('like_count', '>', 0)->orderByDesc('like_count')->take($limit)->get();
return response()->json(['type' => 'top_videos', 'items' => $items->map(fn($v) => [
'thumbnail' => $v->thumbnail ? asset('storage/thumbnails/'.$v->thumbnail) : null,
'title' => $v->title,
'owner' => $v->username,
'views' => 0,
'likes' => $v->like_count,
'url' => route('videos.show', Video::encodeId($v->id)),
])]);
case 'comments':
$items = \DB::table('comments')
->join('users', 'comments.user_id', '=', 'users.id')
->join('videos', 'comments.video_id', '=', 'videos.id')
->select('comments.body', 'users.name as user_name', 'videos.title as video_title', 'videos.id as video_id', 'comments.created_at')
->orderByDesc('comments.created_at')->take($limit)->get();
return response()->json(['type' => 'comments', 'items' => $items->map(fn($c) => [
'user' => $c->user_name,
'body' => \Str::limit($c->body, 120),
'video' => $c->video_title,
'time' => \Carbon\Carbon::parse($c->created_at)->diffForHumans(),
'url' => route('videos.show', Video::encodeId($c->video_id)),
])]);
case 'country_viewers':
$items = \DB::table('video_views')
->join('videos', 'video_views.video_id', '=', 'videos.id')
->leftJoin('users', 'video_views.user_id', '=', 'users.id')
->select('videos.title', 'videos.id as video_id', 'users.name as viewer_name', 'video_views.watched_at')
->where('video_views.country', $request->country)
->orderByDesc('video_views.watched_at')->take($limit)->get();
return response()->json(['type' => 'views', 'items' => $items->map(fn($v) => [
'video' => $v->title,
'viewer' => $v->viewer_name ?? 'Guest',
'country' => $request->country,
'time' => \Carbon\Carbon::parse($v->watched_at)->diffForHumans(),
'url' => route('videos.show', Video::encodeId($v->video_id)),
])]);
case 'uploaders':
$items = \DB::table('users')
->select('users.id', 'users.name', 'users.avatar',
\DB::raw('COUNT(videos.id) as video_count'),
\DB::raw('COALESCE(SUM((SELECT COUNT(*) FROM video_views WHERE video_views.video_id = videos.id)),0) as total_views'))
->leftJoin('videos', 'users.id', '=', 'videos.user_id')
->groupBy('users.id', 'users.name', 'users.avatar')
->orderByDesc('video_count')->take($limit)->get();
return response()->json(['type' => 'uploaders', 'items' => $items->map(fn($u) => [
'avatar' => $u->avatar ? asset('storage/avatars/'.$u->avatar) : 'https://i.pravatar.cc/40?u='.$u->id,
'name' => $u->name,
'videos' => $u->video_count,
'views' => $u->total_views,
'url' => route('channel', $u->channel),
])]);
}
return response()->json(['error' => 'Unknown resource'], 400);
}
public function logs(Request $request)
{
$logFile = storage_path('logs/laravel.log');
$lines = [];
$filter = $request->get('filter', '');
$level = $request->get('level', '');
$limit = (int) $request->get('limit', 200);
if (file_exists($logFile)) {
$all = array_reverse(file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
foreach ($all as $line) {
if ($filter && stripos($line, $filter) === false) continue;
if ($level && stripos($line, ".$level:") === false) continue;
$lines[] = $line;
if (count($lines) >= $limit) break;
}
}
return view('admin.logs', compact('lines', 'filter', 'level', 'limit'));
}
public function auditLogs(Request $request)
{
$query = AuditLog::query()->latest('created_at');
if ($request->filled('action')) {
$query->where('action', $request->action);
}
if ($request->filled('user')) {
$query->where(function ($q) use ($request) {
$q->where('user_name', 'like', '%'.$request->user.'%')
->orWhereHas('user', fn($u) => $u->where('email', 'like', '%'.$request->user.'%'));
});
}
if ($request->filled('ip')) {
$query->where('ip_address', 'like', '%'.$request->ip.'%');
}
if ($request->filled('subject')) {
$query->where('subject_label', 'like', '%'.$request->subject.'%');
}
if ($request->filled('date_from')) {
$query->whereDate('created_at', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('created_at', '<=', $request->date_to);
}
$logs = $query->paginate(50)->withQueryString();
$actionTypes = AuditLog::query()
->select('action')
->distinct()
->orderBy('action')
->pluck('action');
return view('admin.audit-logs', compact('logs', 'actionTypes'));
}
// ── Settings ──────────────────────────────────────────────────────────
public function settings()
{
$settings = [
'gpu_enabled' => Setting::get('gpu_enabled', 'true'),
'gpu_device' => Setting::get('gpu_device', '0'),
'gpu_encoder' => Setting::get('gpu_encoder', 'h264_nvenc'),
'gpu_hwaccel' => Setting::get('gpu_hwaccel', 'cuda'),
'gpu_preset' => Setting::get('gpu_preset', 'p4'),
'ffmpeg_binary' => Setting::get('ffmpeg_binary', config('ffmpeg.ffmpeg', '/usr/bin/ffmpeg')),
];
$gpus = $this->probeGpus();
$nvencWorks = $this->probeNvenc();
return view('admin.settings', compact('settings', 'gpus', 'nvencWorks'));
}
public function updateSettings(Request $request)
{
$request->validate([
'gpu_enabled' => 'required|in:true,false',
'gpu_device' => 'required|integer|min:0|max:15',
'gpu_encoder' => 'required|in:h264_nvenc,hevc_nvenc,libx264',
'gpu_hwaccel' => 'required|in:cuda,none',
'gpu_preset' => 'required|in:p1,p2,p3,p4,p5,p6,p7,fast,medium,slow',
'ffmpeg_binary' => 'required|string|max:255',
]);
$binary = trim($request->ffmpeg_binary) ?: '/usr/bin/ffmpeg';
if (! file_exists($binary) || ! is_executable($binary)) {
return back()->withErrors(['ffmpeg_binary' => "FFmpeg binary not found or not executable: {$binary}"]);
}
Setting::set('gpu_enabled', $request->gpu_enabled);
Setting::set('gpu_device', (string) $request->gpu_device);
Setting::set('gpu_encoder', $request->gpu_encoder);
Setting::set('gpu_hwaccel', $request->gpu_hwaccel);
Setting::set('gpu_preset', $request->gpu_preset);
Setting::set('ffmpeg_binary', $binary);
return back()->with('success', 'Settings saved.');
}
public function detectGpu()
{
return response()->json(['gpus' => $this->probeGpus()]);
}
private function probeGpus(): array
{
$gpus = [];
exec(
'nvidia-smi --query-gpu=index,name,memory.total,memory.free,utilization.gpu,temperature.gpu,driver_version'
. ' --format=csv,noheader,nounits 2>/dev/null',
$lines, $exit
);
if ($exit !== 0 || empty($lines)) return $gpus;
foreach ($lines as $line) {
$parts = array_map('trim', explode(',', $line));
if (count($parts) < 7) continue;
$gpus[] = [
'index' => (int) $parts[0],
'name' => $parts[1],
'mem_total' => (int) $parts[2],
'mem_free' => (int) $parts[3],
'util' => (int) $parts[4],
'temp' => (int) $parts[5],
'driver' => $parts[6],
];
}
return $gpus;
}
/**
* Quick smoke-test: encode one frame with h264_nvenc and return true if it succeeds.
* This catches CUDA compat / driver-version mismatches that nvidia-smi can't detect.
*/
private function probeNvenc(): bool
{
$ffmpeg = Setting::ffmpegBinary();
$tmp = sys_get_temp_dir() . '/nvenc_probe_' . getmypid() . '.mp4';
$device = Setting::gpuDevice();
exec(
escapeshellcmd($ffmpeg)
. ' -f lavfi -i nullsrc=s=128x72:r=1 -frames:v 1'
. " -c:v h264_nvenc -gpu {$device}"
. ' -y ' . escapeshellarg($tmp) . ' 2>/dev/null',
$out, $exit
);
$ok = ($exit === 0 && file_exists($tmp) && filesize($tmp) > 0);
@unlink($tmp);
return $ok;
}
public function nasStorage()
{
$nodes = config('nas-file-manager.schema', []);
return view('admin.nas-storage', compact('nodes'));
}
}