535 lines
23 KiB
PHP
535 lines
23 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Api;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Anime;
|
||
use App\Models\Genre;
|
||
use App\Models\Episode;
|
||
use App\Models\Banner;
|
||
use App\Models\ContinueWatching;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class AnimeApiController extends Controller
|
||
{
|
||
// GET /api/home
|
||
public function home(Request $request)
|
||
{
|
||
$featured = Anime::where('is_featured', true)->where('is_published', true)
|
||
->with('genres', 'seasons', 'episodes')
|
||
->latest()->take(5)->get();
|
||
|
||
if ($featured->isEmpty()) {
|
||
$featured = Anime::where('is_published', true)->with('genres', 'seasons', 'episodes')
|
||
->where('rating', '>=', 1)->orderByDesc('rating')->take(5)->get();
|
||
}
|
||
|
||
$latest = Anime::where('is_published', true)->latest()->take(20)->get();
|
||
$topRated = Anime::where('is_published', true)->where('rating', '>=', 7)
|
||
->orderByDesc('rating')->take(12)->get();
|
||
|
||
try {
|
||
$manualTrending = Anime::where('is_trending', true)->where('is_published', true)
|
||
->orderBy('trending_order')->take(12)->get();
|
||
|
||
if ($manualTrending->count() >= 6) {
|
||
$trending = $manualTrending->take(12);
|
||
} else {
|
||
$autoIds = $manualTrending->pluck('id')->toArray();
|
||
$autoFill = Anime::where('is_published', true)
|
||
->whereNotIn('id', $autoIds)
|
||
->withSum(['episodes as recent_views' => fn($q) =>
|
||
$q->where('is_published', true)->where('updated_at', '>=', now()->subDays(30))
|
||
], 'view_count')
|
||
->orderByDesc('recent_views')
|
||
->take(12 - $manualTrending->count())->get();
|
||
$trending = $manualTrending->concat($autoFill);
|
||
}
|
||
} catch (\Throwable) {
|
||
$trending = collect();
|
||
}
|
||
|
||
if ($trending->isEmpty()) $trending = $latest->take(12);
|
||
|
||
$newEpisodes = Episode::with(['anime', 'season'])
|
||
->where('is_published', true)->latest()->take(12)->get()
|
||
->filter(fn($e) => $e->anime && $e->season)->values();
|
||
|
||
$genres = Genre::where('is_active', true)->take(16)->get();
|
||
|
||
$continueWatching = collect();
|
||
$recommended = collect();
|
||
|
||
$authUser = auth('sanctum')->user();
|
||
if ($authUser) {
|
||
try {
|
||
$continueWatching = ContinueWatching::where('user_id', $authUser->id)
|
||
->with('anime:id,title,slug,cover_image')
|
||
->where('percent_complete', '>=', 5)
|
||
->where('percent_complete', '<', 95)
|
||
->orderByDesc('updated_at')->limit(10)->get();
|
||
|
||
$watchedIds = ContinueWatching::where('user_id', $authUser->id)->pluck('anime_id');
|
||
if ($watchedIds->isNotEmpty()) {
|
||
$topGenreIds = DB::table('anime_genre')
|
||
->whereIn('anime_id', $watchedIds)
|
||
->select('genre_id', DB::raw('count(*) as cnt'))
|
||
->groupBy('genre_id')->orderByDesc('cnt')->limit(3)->pluck('genre_id');
|
||
|
||
if ($topGenreIds->isNotEmpty()) {
|
||
$recommended = Anime::where('is_published', true)
|
||
->whereNotIn('id', $watchedIds)
|
||
->whereHas('genres', fn($q) => $q->whereIn('genres.id', $topGenreIds))
|
||
->where('rating', '>=', 6)->inRandomOrder()->take(12)->get();
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {}
|
||
}
|
||
|
||
return response()->json([
|
||
'featured' => $featured->map(fn($a) => $this->animeResource($a, true)),
|
||
'trending' => $trending->values()->map(fn($a) => $this->animeResource($a)),
|
||
'latest' => $latest->map(fn($a) => $this->animeResource($a)),
|
||
'top_rated' => $topRated->map(fn($a) => $this->animeResource($a)),
|
||
'new_episodes' => $newEpisodes->map(fn($e) => $this->episodeCardResource($e)),
|
||
'genres' => $genres->map(fn($g) => ['id'=>$g->id,'name'=>$g->name,'slug'=>$g->slug]),
|
||
'continue_watching'=> $continueWatching->map(fn($cw) => $this->continueWatchingResource($cw)),
|
||
'recommended' => $recommended->map(fn($a) => $this->animeResource($a)),
|
||
]);
|
||
}
|
||
|
||
// GET /api/animes
|
||
public function index(Request $request)
|
||
{
|
||
$q = $request->input('q', '');
|
||
$genre = $request->input('genre');
|
||
$type = $request->input('type');
|
||
$status = $request->input('status');
|
||
$year = $request->input('year');
|
||
$sort = $request->input('sort', 'latest'); // latest|rating|views
|
||
|
||
$query = Anime::where('is_published', true)->with('genres');
|
||
|
||
if ($q) {
|
||
$query->where(function ($qb) use ($q) {
|
||
$qb->where('title', 'like', "%$q%")
|
||
->orWhere('title_en', 'like', "%$q%")
|
||
->orWhere('title_jp', 'like', "%$q%");
|
||
});
|
||
}
|
||
if ($genre) $query->whereHas('genres', fn($qb) => $qb->where('slug', $genre));
|
||
if ($type) $query->where('type', $type);
|
||
if ($status) $query->where('status', $status);
|
||
if ($year) $query->where('release_year', $year);
|
||
|
||
match ($sort) {
|
||
'rating' => $query->orderByDesc('rating'),
|
||
default => $query->latest(),
|
||
};
|
||
|
||
$results = $query->paginate(24)->withQueryString();
|
||
|
||
return response()->json([
|
||
'data' => collect($results->items())->map(fn($a) => $this->animeResource($a)),
|
||
'total' => $results->total(),
|
||
'per_page' => $results->perPage(),
|
||
'current_page'=> $results->currentPage(),
|
||
'last_page' => $results->lastPage(),
|
||
]);
|
||
}
|
||
|
||
// GET /api/animes/{slug}
|
||
public function show(Request $request, string $slug)
|
||
{
|
||
$anime = Anime::where('slug', $slug)->where('is_published', true)
|
||
->with(['genres', 'seasons', 'seasons.episodes' => fn($q) => $q->where('is_published', true)->orderBy('episode_number')])
|
||
->firstOrFail();
|
||
|
||
$userId = $request->user()?->id;
|
||
|
||
$inWatchlist = false;
|
||
$userRating = null;
|
||
$isFollowing = false;
|
||
|
||
if ($userId) {
|
||
$wl = \App\Models\Watchlist::where('user_id', $userId)->where('anime_id', $anime->id)->first();
|
||
$inWatchlist = $wl !== null;
|
||
$watchlistStatus = $wl?->status;
|
||
$userRating = \App\Models\AnimeRating::where('user_id', $userId)->where('anime_id', $anime->id)->value('rating');
|
||
$isFollowing = \App\Models\AnimeFollow::where('user_id', $userId)->where('anime_id', $anime->id)->exists();
|
||
}
|
||
|
||
$seasons = $anime->seasons->map(function ($season) {
|
||
return [
|
||
'id' => $season->id,
|
||
'season_number' => $season->season_number,
|
||
'title' => $season->title,
|
||
'episodes' => $season->episodes->map(fn($ep) => $this->episodeResource($ep)),
|
||
];
|
||
});
|
||
|
||
return response()->json([
|
||
'anime' => $this->animeResource($anime, true),
|
||
'seasons' => $seasons,
|
||
'in_watchlist' => $inWatchlist,
|
||
'watchlist_status'=> $watchlistStatus ?? null,
|
||
'user_rating' => $userRating,
|
||
'is_following' => $isFollowing,
|
||
]);
|
||
}
|
||
|
||
// GET /api/genres/{slug}
|
||
public function genre(Request $request, string $slug)
|
||
{
|
||
$genre = Genre::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
||
$animes = $genre->animes()->where('is_published', true)->latest()->paginate(24);
|
||
|
||
return response()->json([
|
||
'genre' => ['id'=>$genre->id,'name'=>$genre->name,'slug'=>$genre->slug],
|
||
'data' => collect($animes->items())->map(fn($a) => $this->animeResource($a)),
|
||
'total' => $animes->total(),
|
||
'last_page' => $animes->lastPage(),
|
||
'current_page' => $animes->currentPage(),
|
||
]);
|
||
}
|
||
|
||
// GET /api/genres
|
||
public function genres()
|
||
{
|
||
$genres = Genre::where('is_active', true)->orderBy('name')->get();
|
||
return response()->json($genres->map(fn($g) => ['id'=>$g->id,'name'=>$g->name,'slug'=>$g->slug]));
|
||
}
|
||
|
||
// GET /api/watch/{slug}/{season}/{episode}
|
||
public function watch(Request $request, string $slug, int $season, int $episode)
|
||
{
|
||
$anime = Anime::where('slug', $slug)->where('is_published', true)->firstOrFail();
|
||
$seasonModel = $anime->seasons()->where('season_number', $season)->firstOrFail();
|
||
$ep = $seasonModel->episodes()->where('episode_number', $episode)->where('is_published', true)->firstOrFail();
|
||
|
||
$ep->increment('view_count');
|
||
|
||
$prev = $seasonModel->episodes()->where('episode_number', '<', $episode)->where('is_published', true)->orderByDesc('episode_number')->first();
|
||
$next = $seasonModel->episodes()->where('episode_number', '>', $episode)->where('is_published', true)->orderBy('episode_number')->first();
|
||
|
||
// Cross-season next
|
||
if (!$next) {
|
||
$nextSeason = $anime->seasons()->where('season_number', $season + 1)->first();
|
||
if ($nextSeason) {
|
||
$next = $nextSeason->episodes()->where('episode_number', 1)->where('is_published', true)->first();
|
||
}
|
||
}
|
||
|
||
// Subtitles
|
||
$subtitles = [];
|
||
if (method_exists($ep, 'subtitles')) {
|
||
$subtitles = $ep->subtitles()->get()->map(fn($s) => [
|
||
'label' => $s->label,
|
||
'lang' => $s->language,
|
||
'url' => $s->url,
|
||
'is_default' => (bool)($s->is_default ?? false),
|
||
])->values()->toArray();
|
||
}
|
||
|
||
// Dub sources from m3u8 URL
|
||
$dubSources = [];
|
||
if ($ep->m3u8_url) {
|
||
$rawDubs = $ep->available_dubs ?? null;
|
||
$availableDubs = is_string($rawDubs) ? json_decode($rawDubs, true) : (is_array($rawDubs) ? $rawDubs : null);
|
||
[$dubSources] = \App\Http\Controllers\Frontend\PlayerController::resolveDubSourcesPublic(
|
||
$ep->m3u8_url, $availableDubs
|
||
);
|
||
}
|
||
|
||
// Quality sources (legacy — episode.video_url / m3u8_url)
|
||
$sources = collect();
|
||
if ($ep->m3u8_url) $sources->push(['quality'=>'Auto (HLS)','url'=>$ep->m3u8_url,'type'=>'hls']);
|
||
if ($ep->video_url) {
|
||
$isHls = str_ends_with($ep->video_url, '.m3u8') || str_contains($ep->video_url, 'master.m3u8');
|
||
$type = $isHls ? 'hls' : 'mp4';
|
||
$sources->push(['quality'=>'Auto','url'=>$ep->video_url,'type'=>$type]);
|
||
}
|
||
if ($ep->video_url_1080 ?? null) $sources->push(['quality'=>'1080p','url'=>$ep->video_url_1080,'type'=>'mp4']);
|
||
if ($ep->video_url_720 ?? null) $sources->push(['quality'=>'720p','url'=>$ep->video_url_720,'type'=>'mp4']);
|
||
if ($ep->video_url_480 ?? null) $sources->push(['quality'=>'480p','url'=>$ep->video_url_480,'type'=>'mp4']);
|
||
|
||
// Çok kaynak desteği (video_sources tablosu)
|
||
// Her translator/kaynak bir grup → [{key, label, url, type, quality}]
|
||
$multiSources = \App\Models\VideoSource::where('episode_id', $ep->id)
|
||
->orderBy('sort_order')
|
||
->get()
|
||
->groupBy(fn($vs) => $vs->translator_id ?: $vs->label)
|
||
->map(function ($group) {
|
||
$default = $group->firstWhere('is_default', true) ?? $group->first();
|
||
// Tüm kaliteler (1080p, 720p, vb.)
|
||
$qualities = $group->map(fn($vs) => [
|
||
'quality' => $vs->quality ?: 'Auto',
|
||
'url' => $vs->url,
|
||
'type' => $vs->type ?? 'mp4',
|
||
])->values()->toArray();
|
||
|
||
return [
|
||
'key' => $default->translator_id
|
||
?: \Illuminate\Support\Str::slug($default->label ?? 'kaynak'),
|
||
'label' => $default->label ?: 'Kaynak',
|
||
'url' => $default->url,
|
||
'type' => $default->type ?? 'mp4',
|
||
'quality' => $default->quality ?: 'Auto',
|
||
'source' => $default->source ?? 'animecix',
|
||
'qualities' => $qualities,
|
||
'is_default'=> (bool) $default->is_default,
|
||
];
|
||
})
|
||
->values()
|
||
->toArray();
|
||
|
||
// Player settings
|
||
$skipSeconds = (int) \App\Models\Setting::get('main_video_skip_seconds', 10);
|
||
$wmCoverSeconds = (int) \App\Models\Setting::get('watermark_cover_seconds', 11);
|
||
$introEnabled = \App\Models\Setting::get('intro_enabled') == '1';
|
||
$introUrl = $introEnabled ? (\App\Models\Setting::get('intro_video_url') ?: null) : null;
|
||
$introSkipAfter = (int) \App\Models\Setting::get('intro_skip_after', 5);
|
||
|
||
// AniSkip is fetched via separate /api/aniskip endpoint to avoid blocking video load
|
||
$aniSkipData = null;
|
||
|
||
// All episodes list for in-player episode switcher
|
||
$allEpisodes = $seasonModel->episodes()->where('is_published', true)->orderBy('episode_number')
|
||
->get()->map(fn($e) => [
|
||
'id' => $e->id,
|
||
'episode_number' => $e->episode_number,
|
||
'season_number' => $season,
|
||
'title' => $e->title,
|
||
'thumbnail_url' => $e->thumbnail_url ?? null,
|
||
]);
|
||
|
||
return response()->json([
|
||
'anime' => ['id'=>$anime->id,'title'=>$anime->title,'slug'=>$anime->slug,'cover_url'=>$anime->coverUrl],
|
||
'season' => ['id'=>$seasonModel->id,'season_number'=>$seasonModel->season_number,'title'=>$seasonModel->title],
|
||
'episode' => $this->episodeResource($ep),
|
||
'sources' => $sources->values(),
|
||
'multi_sources'=> $multiSources, // Çok kaynak (Anizium "4K" + AnimeCix çevirmenler)
|
||
'dub_sources' => $dubSources,
|
||
'subtitles' => $subtitles,
|
||
'episodes' => $allEpisodes,
|
||
'prev_episode' => $prev ? ['season'=>$prev->season?->season_number ?? $season,'episode'=>$prev->episode_number] : null,
|
||
'next_episode' => $next ? ['season'=>$next->season?->season_number ?? $season,'episode'=>$next->episode_number] : null,
|
||
'settings' => [
|
||
'skip_seconds' => $skipSeconds,
|
||
'wm_cover_seconds' => $wmCoverSeconds,
|
||
'intro_url' => $introUrl,
|
||
'intro_skip_after' => $introSkipAfter,
|
||
// AniSkip timestamps (null if not available)
|
||
'aniskip' => $aniSkipData, // {'op':{'start':X,'end':Y}, 'ed':{'start':X,'end':Y}}
|
||
],
|
||
]);
|
||
}
|
||
|
||
// ── Resources ─────────────────────────────────────────────────────────────
|
||
|
||
private function animeResource(Anime $a, bool $full = false): array
|
||
{
|
||
$base = [
|
||
'id' => $a->id,
|
||
'title' => $a->title,
|
||
'title_en' => $a->title_en,
|
||
'title_jp' => $a->title_jp,
|
||
'slug' => $a->slug,
|
||
'cover_url' => $a->coverUrl,
|
||
'banner_url' => $a->bannerUrl,
|
||
'type' => $a->type,
|
||
'status' => $a->status,
|
||
'rating' => $a->rating ? (float)$a->rating : null,
|
||
'release_year' => $a->release_year,
|
||
'episode_count' => $a->episode_count,
|
||
'genres' => $a->relationLoaded('genres')
|
||
? $a->genres->map(fn($g) => ['id'=>$g->id,'name'=>$g->name,'slug'=>$g->slug])->values()
|
||
: [],
|
||
];
|
||
|
||
if ($full) {
|
||
$base['description'] = $a->description;
|
||
$base['studio'] = $a->studio ?? null;
|
||
$base['duration'] = $a->duration ?? null;
|
||
$base['is_featured'] = $a->is_featured;
|
||
|
||
// First episode for "watch now" button
|
||
if ($a->relationLoaded('seasons') && $a->seasons->isNotEmpty()) {
|
||
$firstSeason = $a->seasons->first();
|
||
$eps = $a->relationLoaded('episodes') ? $a->episodes : $firstSeason->episodes;
|
||
$firstEp = $eps->where('season_id', $firstSeason->id)->where('is_published', true)->sortBy('episode_number')->first();
|
||
$base['first_watch'] = ($firstSeason && $firstEp) ? [
|
||
'season' => $firstSeason->season_number,
|
||
'episode' => $firstEp->episode_number,
|
||
] : null;
|
||
}
|
||
}
|
||
|
||
return $base;
|
||
}
|
||
|
||
private function episodeResource(Episode $ep): array
|
||
{
|
||
return [
|
||
'id' => $ep->id,
|
||
'episode_number' => $ep->episode_number,
|
||
'title' => $ep->title,
|
||
'thumbnail_url' => $ep->thumbnailUrl ?? null,
|
||
'duration' => $ep->duration,
|
||
'view_count' => $ep->view_count,
|
||
'created_at' => $ep->created_at?->toISOString(),
|
||
];
|
||
}
|
||
|
||
private function episodeCardResource(Episode $ep): array
|
||
{
|
||
return [
|
||
'id' => $ep->id,
|
||
'episode_number' => $ep->episode_number,
|
||
'season_number' => $ep->season?->season_number,
|
||
'title' => $ep->title,
|
||
'thumbnail_url' => $ep->thumbnailUrl ?? null,
|
||
'created_at' => $ep->created_at?->diffForHumans(),
|
||
'anime' => [
|
||
'id' => $ep->anime->id,
|
||
'title' => $ep->anime->title,
|
||
'slug' => $ep->anime->slug,
|
||
'cover_url' => $ep->anime->coverUrl,
|
||
'rating' => $ep->anime->rating ? (float)$ep->anime->rating : null,
|
||
'description' => $ep->anime->description,
|
||
],
|
||
];
|
||
}
|
||
|
||
// ── AniSkip endpoint ─────────────────────────────────────────────────────
|
||
|
||
// GET /api/aniskip/{slug}/{season}/{episode}
|
||
// Fully automatic: finds MAL ID by title if missing, caches everything
|
||
public function aniSkip(string $slug, int $season, int $episode)
|
||
{
|
||
$anime = Anime::where('slug', $slug)->first();
|
||
if (!$anime) return response()->json(['aniskip' => null]);
|
||
|
||
$seasonModel = $anime->seasons()->where('season_number', $season)->first();
|
||
if (!$seasonModel) return response()->json(['aniskip' => null]);
|
||
|
||
$seasonMalId = $seasonModel->mal_id;
|
||
|
||
try {
|
||
// anime.mal_id yoksa title search (bir kez, cache'lenir)
|
||
if (!$anime->mal_id) {
|
||
$found = (new \App\Services\JikanService())->searchMalId($anime->title, $anime->title_en, $anime->title_jp);
|
||
if ($found) $anime->update(['mal_id' => $found]);
|
||
}
|
||
|
||
if (!$seasonMalId && $anime->mal_id) {
|
||
// S1 için anime.mal_id direkt kullan — Jikan'a gitme
|
||
if ($season === 1) {
|
||
$seasonMalId = $anime->mal_id;
|
||
$seasonModel->update(['mal_id' => $seasonMalId]);
|
||
} else {
|
||
// Diğer sezonlar: sadece cache'ten bak, yoksa null dön (page load bloke olmasın)
|
||
$chain = \Illuminate\Support\Facades\Cache::get("jikan_chain_{$anime->mal_id}");
|
||
if ($chain) {
|
||
$seasonMalId = $chain[$season - 1] ?? $chain[0] ?? null;
|
||
if ($seasonMalId) $seasonModel->update(['mal_id' => $seasonMalId]);
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($seasonMalId) {
|
||
$data = (new \App\Services\AniSkipService())->getSkipTimes((string)$seasonMalId, $episode);
|
||
return response()->json(['aniskip' => $data]);
|
||
}
|
||
} catch (\Throwable) {}
|
||
|
||
return response()->json(['aniskip' => null]);
|
||
}
|
||
|
||
// ── Skip segments ─────────────────────────────────────────────────────────
|
||
|
||
// POST /api/episodes/{episode}/skip-event (anonim OK, rate-limited)
|
||
public function recordSkipEvent(Request $request, Episode $episode)
|
||
{
|
||
$from = (int) $request->input('from_sec', 0);
|
||
$to = (int) $request->input('to_sec', 0);
|
||
|
||
// Basic sanity: must skip forward at least 5s, not more than 10 min
|
||
if ($to <= $from + 4 || ($to - $from) > 600) {
|
||
return response()->json(['ok' => false]);
|
||
}
|
||
|
||
DB::table('episode_skip_events')->insert([
|
||
'episode_id' => $episode->id,
|
||
'from_sec' => $from,
|
||
'to_sec' => $to,
|
||
'created_at' => now(),
|
||
]);
|
||
|
||
return response()->json(['ok' => true]);
|
||
}
|
||
|
||
// GET /api/episodes/{episode}/skip-segments
|
||
// Returns segments where >= 10 users skipped from within a 30-second window
|
||
public function skipSegments(Episode $episode)
|
||
{
|
||
// İntro tespiti: ilk 3 dakika içinde 45-150sn ileri atlama = intro skip
|
||
// Kümeleme: 20sn bucket, to_sec standart sapması <= 15sn, en az 2 farklı kullanıcı
|
||
$rows = DB::table('episode_skip_events')
|
||
->where('episode_id', $episode->id)
|
||
->where('from_sec', '<', 180)
|
||
->whereRaw('(to_sec - from_sec) BETWEEN 45 AND 150')
|
||
->selectRaw('
|
||
FLOOR(from_sec / 20) * 20 AS bucket_start,
|
||
AVG(to_sec) AS avg_to,
|
||
STDDEV_POP(to_sec) AS stddev_to,
|
||
COUNT(*) AS cnt
|
||
')
|
||
->groupByRaw('FLOOR(from_sec / 20) * 20')
|
||
->havingRaw('cnt >= 2 AND (STDDEV_POP(to_sec) <= 15 OR cnt = 1)')
|
||
->orderBy('cnt', 'desc')
|
||
->limit(1)
|
||
->get();
|
||
|
||
$segments = $rows->map(fn($r) => [
|
||
'from' => (int) $r->bucket_start,
|
||
'to' => (int) round($r->avg_to),
|
||
'count'=> (int) $r->cnt,
|
||
])->values();
|
||
|
||
return response()->json(['segments' => $segments]);
|
||
}
|
||
|
||
private function continueWatchingResource($cw): array
|
||
{
|
||
return [
|
||
'id' => $cw->id,
|
||
'season_number' => $cw->season_number,
|
||
'episode_number' => $cw->episode_number,
|
||
'percent_complete' => $cw->percent_complete,
|
||
'anime' => $cw->anime ? [
|
||
'id' => $cw->anime->id,
|
||
'title' => $cw->anime->title,
|
||
'slug' => $cw->anime->slug,
|
||
'cover_url' => \App\Support\MediaUrl::fromStoragePath($cw->anime->cover_image),
|
||
] : null,
|
||
];
|
||
}
|
||
|
||
// POST /api/sources/flag-hevc
|
||
// Player tarafından çağrılır: HEVC hatası alınan kaynak URL'sini DB'ye işler
|
||
public function flagHevc(Request $request)
|
||
{
|
||
$url = $request->input('url');
|
||
if (!$url) return response()->json(['ok' => false]);
|
||
|
||
\App\Models\VideoSource::where('url', $url)->update([
|
||
'is_hevc' => true,
|
||
'hevc_checked_at' => now(),
|
||
]);
|
||
|
||
return response()->json(['ok' => true]);
|
||
}
|
||
}
|