Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Achievement extends Model
{
protected $fillable = [
'key', 'title', 'description', 'icon', 'color',
'condition_type', 'condition_value',
];
public function userAchievements()
{
return $this->hasMany(UserAchievement::class);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivationCode extends Model
{
protected $fillable = [
'code', 'plan_id', 'used_by', 'used_at',
'created_by', 'expires_at', 'batch', 'notes',
];
protected $casts = [
'used_at' => 'datetime',
'expires_at' => 'datetime',
];
public function plan(): BelongsTo
{
return $this->belongsTo(MembershipPlan::class, 'plan_id');
}
public function usedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'used_by');
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function isUsed(): bool
{
return ! is_null($this->used_at);
}
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
public function isValid(): bool
{
return ! $this->isUsed() && ! $this->isExpired();
}
public static function generateCode(): string
{
do {
$hex = strtoupper(bin2hex(random_bytes(6)));
$code = implode('-', str_split($hex, 4));
} while (self::where('code', $code)->exists());
return $code;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Ad extends Model
{
protected $fillable = [
'name', 'type', 'placement', 'file_path', 'external_url', 'click_url',
'skip_after', 'weight', 'is_active', 'starts_at', 'ends_at',
'impressions', 'clicks',
];
protected $casts = [
'is_active' => 'boolean',
'starts_at' => 'datetime',
'ends_at' => 'datetime',
];
/** Aktif + zamanlaması uygun reklamlar */
public function scopeLive(Builder $q): Builder
{
return $q->where('is_active', true)
->where(fn($s) => $s->whereNull('starts_at')->orWhere('starts_at', '<=', now()))
->where(fn($s) => $s->whereNull('ends_at')->orWhere('ends_at', '>=', now()));
}
/**
* Medya URL'si yüklenen dosya veya dış URL.
* Yüklenen dosyalar /media/{path} route'undan servis edilir (MediaController);
* public/storage symlink'ine bağımlı değil kapaklar/avatarlarla aynı yol.
*/
public function getMediaUrlAttribute(): ?string
{
if ($this->file_path) return MediaUrl::fromStoragePath($this->file_path);
if ($this->external_url) return $this->external_url;
return null;
}
/** CTR yüzdesi */
public function getCtrAttribute(): float
{
return $this->impressions > 0
? round($this->clicks / $this->impressions * 100, 2)
: 0.0;
}
/** Ağırlıklı rastgele seçim — pre-roll video reklam */
public static function pickVideo(): ?self
{
return self::weightedPick(
self::live()->where('type', 'video')->where('placement', 'preroll')->get()
);
}
/** Ağırlıklı rastgele seçim — banner (placement bazlı) */
public static function pickBanner(string $placement): ?self
{
return self::weightedPick(
self::live()->where('type', 'banner')->where('placement', $placement)->get()
);
}
private static function weightedPick($ads): ?self
{
if ($ads->isEmpty()) return null;
$total = max(1, $ads->sum('weight'));
$roll = random_int(1, $total);
foreach ($ads as $ad) {
$roll -= max(1, $ad->weight);
if ($roll <= 0) return $ad;
}
return $ads->first();
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
class AiQuery extends Model
{
public $timestamps = false;
protected $table = 'analytics_ai_queries';
protected $fillable = [
'user_id', 'query_type', 'query_text', 'created_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
class BotLog extends Model
{
protected $table = 'analytics_bot_logs';
public $timestamps = false;
protected $fillable = [
'ip', 'user_agent', 'path', 'method', 'action', 'bot_name', 'created_at',
];
protected $casts = ['created_at' => 'datetime'];
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
use App\Models\Anime;
class PageView extends Model
{
public $timestamps = false;
protected $table = 'analytics_pageviews';
protected $fillable = [
'user_id', 'session_id', 'url', 'page_type',
'anime_id', 'episode_id', 'ip', 'country', 'city',
'device', 'browser', 'referrer', 'is_bot', 'user_agent',
'time_on_page', 'created_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
class VisitorSession extends Model
{
protected $table = 'analytics_sessions';
public $timestamps = false;
protected $fillable = [
'session_id', 'user_id', 'ip', 'country', 'city',
'device', 'browser', 'referrer', 'landing_page',
'pages_visited', 'total_seconds', 'is_bot', 'bot_type',
'user_agent', 'started_at', 'last_seen_at',
];
protected $casts = [
'is_bot' => 'boolean',
'started_at' => 'datetime',
'last_seen_at'=> 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
use App\Models\Anime;
use App\Models\Episode;
class WatchEvent extends Model
{
public $timestamps = false;
protected $table = 'analytics_watch_events';
protected $fillable = [
'user_id', 'session_id', 'anime_id', 'episode_id',
'season_number', 'episode_number',
'seconds_watched', 'total_seconds', 'percent_complete',
'created_at',
];
protected $casts = [
'created_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
public function episode() { return $this->belongsTo(Episode::class); }
}
+103
View File
@@ -0,0 +1,103 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Anime extends Model
{
protected $fillable = [
'title', 'title_en', 'title_jp', 'slug', 'description',
'cover_image', 'banner_image', 'trailer_url',
'release_year', 'type', 'status', 'episode_count',
'rating', 'studio', 'mal_id', 'is_featured', 'is_published', 'is_dubbed',
'is_trending', 'trending_order',
];
protected $casts = [
'is_featured' => 'boolean',
'is_published' => 'boolean',
'is_dubbed' => 'boolean',
'is_trending' => 'boolean',
'rating' => 'float',
'trending_order' => 'integer',
];
protected static function boot()
{
parent::boot();
static::creating(function ($anime) {
if (empty($anime->slug)) {
$base = Str::slug($anime->title ?: 'anime');
$slug = $base;
$i = 2;
while (static::where('slug', $slug)->exists()) {
$slug = $base . '-' . $i++;
}
$anime->slug = $slug;
}
});
static::saving(function ($anime) {
if (empty($anime->slug)) {
$base = Str::slug($anime->title ?: 'anime');
$slug = $base;
$i = 2;
while (static::where('slug', $slug)->whereKeyNot($anime->id ?? 0)->exists()) {
$slug = $base . '-' . $i++;
}
$anime->slug = $slug;
}
});
}
/** Güvenli detail URL — slug null olsa bile çökmez. */
public function getDetailUrlAttribute(): string
{
return $this->slug ? route('anime.show', $this->slug) : '#';
}
public function genres()
{
return $this->belongsToMany(Genre::class, 'anime_genre');
}
public function seasons()
{
return $this->hasMany(Season::class)->orderBy('season_number');
}
public function episodes()
{
return $this->hasMany(Episode::class);
}
public function importJobs()
{
return $this->hasMany(ImportJob::class);
}
public function permissions()
{
return $this->morphMany(ContentPermission::class, 'content', 'content_type', 'content_id');
}
/** Cover veya banner URL'sini döndürür (storage veya dış URL) */
private function imageUrl(?string $path): ?string
{
return MediaUrl::fromStoragePath($path);
}
public function getCoverUrlAttribute(): ?string { return $this->imageUrl($this->cover_image); }
public function getBannerUrlAttribute(): ?string { return $this->imageUrl($this->banner_image); }
public function getPermission(string $key): string
{
$override = $this->permissions()->where('permission_key', $key)->first();
if ($override) return $override->required_membership;
$global = PermissionSetting::where('key', $key)->first();
return $global ? $global->required_membership : 'free';
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnimeFollow extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'anime_id'];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
}
+13
View File
@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnimeRating extends Model
{
protected $fillable = ['user_id', 'anime_id', 'rating'];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnimeRequest extends Model
{
protected $fillable = [
'user_id', 'title', 'original_title', 'note',
'status', 'admin_note', 'vote_count',
];
const STATUSES = [
'pending' => ['label' => 'Bekliyor', 'color' => '#f0883e'],
'approved' => ['label' => 'Onaylandı', 'color' => '#3fb950'],
'rejected' => ['label' => 'Reddedildi', 'color' => '#f85149'],
'added' => ['label' => 'Eklendi', 'color' => '#79c0ff'],
];
public function user() { return $this->belongsTo(User::class); }
public function votes() { return $this->hasMany(AnimeRequestVote::class); }
public function hasVotedBy(?User $user, string $ip): bool
{
if ($user) {
return $this->votes()->where('user_id', $user->id)->exists();
}
return $this->votes()->where('ip', $ip)->exists();
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnimeRequestVote extends Model
{
public $timestamps = false;
protected $fillable = ['anime_request_id', 'user_id', 'ip', 'created_at'];
protected $casts = ['created_at' => 'datetime'];
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnimeSwipe extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'anime_id', 'direction'];
protected $casts = ['created_at' => 'datetime'];
public function anime() { return $this->belongsTo(Anime::class); }
public function user() { return $this->belongsTo(User::class); }
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Model;
class Banner extends Model
{
protected $fillable = ['title', 'image', 'link', 'is_active', 'sort_order'];
protected $casts = ['is_active' => 'boolean'];
public function getImageUrlAttribute(): ?string
{
return MediaUrl::fromStoragePath($this->image);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class BlogPost extends Model
{
protected $fillable = [
'title', 'slug', 'excerpt', 'content', 'cover_image',
'focus_keyword', 'meta_title', 'meta_description', 'meta_keywords',
'status', 'ai_generated', 'anime_id', 'linked_anime_ids', 'faq',
'views', 'reading_time', 'published_at',
];
protected $casts = [
'linked_anime_ids' => 'array',
'faq' => 'array',
'ai_generated' => 'boolean',
'published_at' => 'datetime',
];
public function getCoverUrlAttribute(): ?string
{
return MediaUrl::fromStoragePath($this->cover_image);
}
public function anime(): BelongsTo
{
return $this->belongsTo(Anime::class);
}
public function scopePublished($q)
{
return $q->where('status', 'published')->whereNotNull('published_at');
}
public function getReadableTimeAttribute(): string
{
return $this->reading_time . ' dk okuma';
}
public static function generateSlug(string $title): string
{
$slug = Str::slug($title, '-', 'tr');
$base = $slug;
$i = 1;
while (static::where('slug', $slug)->exists()) {
$slug = $base . '-' . $i++;
}
return $slug;
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = [
'user_id', 'commentable_type', 'commentable_id',
'parent_id', 'content', 'gif_url', 'status', 'is_pinned', 'like_count',
];
protected $casts = ['is_pinned' => 'boolean'];
public function likes()
{
return $this->hasMany(CommentLike::class);
}
public function isLikedBy(?int $userId): bool
{
if (!$userId) return false;
return $this->likes()->where('user_id', $userId)->exists();
}
public function commentable()
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
public function parent()
{
return $this->belongsTo(Comment::class, 'parent_id');
}
public function replies()
{
return $this->hasMany(Comment::class, 'parent_id');
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CommentLike extends Model
{
protected $fillable = ['user_id', 'comment_id'];
public function comment()
{
return $this->belongsTo(Comment::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContentPermission extends Model
{
protected $fillable = ['content_type', 'content_id', 'permission_key', 'required_membership'];
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContinueWatching extends Model
{
public $timestamps = false;
protected $table = 'continue_watching';
protected $fillable = [
'user_id', 'anime_id', 'episode_id',
'season_number', 'episode_number',
'seconds_watched', 'total_seconds', 'percent_complete',
'updated_at',
];
protected $casts = [
'updated_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
public function episode() { return $this->belongsTo(Episode::class); }
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Conversation extends Model
{
public function participants()
{
return $this->belongsToMany(User::class, 'conversation_participants')
->withPivot('last_read_at');
}
public function messages()
{
return $this->hasMany(Message::class)->orderBy('created_at');
}
public function lastMessage()
{
return $this->hasOne(Message::class)->latestOfMany('created_at');
}
public function unreadCountFor(int $userId): int
{
$pivot = $this->participants->firstWhere('id', $userId)?->pivot;
$lastRead = $pivot?->last_read_at;
$q = $this->messages()->where('user_id', '!=', $userId);
if ($lastRead) {
$q->where('created_at', '>', $lastRead);
}
return $q->count();
}
// Find existing DM between two users or return null
public static function between(int $a, int $b): ?self
{
return self::whereHas('participants', fn($q) => $q->where('user_id', $a))
->whereHas('participants', fn($q) => $q->where('user_id', $b))
->whereHas('participants', fn($q) => $q->havingRaw('COUNT(*) = 2'), null, null, fn($q) => $q->select(\DB::raw('COUNT(*)')))
->first();
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Model;
class Episode extends Model
{
protected $fillable = [
'anime_id', 'season_id', 'episode_number', 'title', 'description',
'thumbnail', 'duration', 'bunny_video_id', 'bunny_library_id',
'video_url', 'm3u8_url', 'available_dubs', 'source_url', 'source', 'status',
'view_count', 'is_published', 'intro_start', 'intro_end',
];
protected $casts = [
'is_published' => 'boolean',
'available_dubs' => 'array',
];
public function anime()
{
return $this->belongsTo(Anime::class);
}
public function season()
{
return $this->belongsTo(Season::class);
}
public function subtitles()
{
return $this->hasMany(Subtitle::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function permissions()
{
return $this->morphMany(ContentPermission::class, 'content', 'content_type', 'content_id');
}
public function getPermission(string $key): string
{
$override = ContentPermission::where('content_type', 'episode')
->where('content_id', $this->id)
->where('permission_key', $key)
->first();
if ($override) return $override->required_membership;
// Anime-level permission
$animeOverride = ContentPermission::where('content_type', 'anime')
->where('content_id', $this->anime_id)
->where('permission_key', $key)
->first();
if ($animeOverride) return $animeOverride->required_membership;
$global = PermissionSetting::where('key', $key)->first();
return $global ? $global->required_membership : 'free';
}
public function getDurationFormattedAttribute(): string
{
if (!$this->duration) return '-';
$minutes = intdiv($this->duration, 60);
$seconds = $this->duration % 60;
return sprintf('%d:%02d', $minutes, $seconds);
}
public function getThumbnailUrlAttribute(): ?string
{
return MediaUrl::fromStoragePath($this->thumbnail);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EpisodeNote extends Model
{
protected $fillable = ['user_id', 'episode_id', 'anime_id', 'content', 'timestamp_at'];
public function user() { return $this->belongsTo(User::class); }
public function episode() { return $this->belongsTo(Episode::class); }
public function anime() { return $this->belongsTo(Anime::class); }
public function getTimestampLabelAttribute(): string
{
if (!$this->timestamp_at) return '';
$s = $this->timestamp_at;
return sprintf('%d:%02d', intdiv($s, 60), $s % 60);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EpisodePrediction extends Model
{
protected $fillable = ['episode_id', 'user_id', 'body', 'is_correct', 'vote_count'];
protected $casts = ['is_correct' => 'boolean'];
public function episode() { return $this->belongsTo(Episode::class); }
public function user() { return $this->belongsTo(User::class); }
public function votes() { return $this->hasMany(PredictionVote::class, 'prediction_id'); }
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EpisodeTimestampComment extends Model
{
public $timestamps = false;
protected $fillable = [
'episode_id', 'user_id', 'timestamp_sec', 'body', 'color', 'is_hidden',
];
protected $casts = [
'is_hidden' => 'boolean',
'timestamp_sec' => 'integer',
];
public function episode() { return $this->belongsTo(Episode::class); }
public function user() { return $this->belongsTo(User::class); }
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class EpisodeVote extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'episode_id', 'vote', 'created_at'];
protected $casts = ['created_at' => 'datetime', 'vote' => 'integer'];
public function user() { return $this->belongsTo(User::class); }
public function episode() { return $this->belongsTo(Episode::class); }
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class FirstWatchSession extends Model
{
public $timestamps = false;
protected $fillable = ['episode_id', 'user_id', 'session_id', 'is_first_time', 'last_seen'];
protected $casts = ['is_first_time' => 'boolean', 'last_seen' => 'datetime'];
public function episode() { return $this->belongsTo(Episode::class); }
public function user() { return $this->belongsTo(User::class); }
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Genre extends Model
{
protected $fillable = ['name', 'slug', 'color', 'is_active'];
protected $casts = ['is_active' => 'boolean'];
protected static function boot()
{
parent::boot();
static::creating(function ($genre) {
if (empty($genre->slug)) {
$genre->slug = Str::slug($genre->name);
}
});
}
public function animes()
{
return $this->belongsToMany(Anime::class, 'anime_genre');
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ImportJob extends Model
{
// Priority sabitleri
const PRIORITY_NEW = 0; // Yeni anime keşfi
const PRIORITY_ONGOING = 1; // Ongoing anime güncelleme
const PRIORITY_CROSSFILL = 2; // Mevcut anime, eksik kaynak ekleme
protected $fillable = [
'source', 'source_url', 'watch_id', 'cdn_id', 'anime_title', 'anime_id',
'animecix_title_id', 'animecix_slug',
'season_ranges', 'status', 'priority', 'total_episodes', 'done_episodes',
'failed_episodes', 'current_step', 'error_log',
];
protected $casts = [
'season_ranges' => 'array',
];
public function anime()
{
return $this->belongsTo(Anime::class);
}
public function getProgressPercentAttribute(): int
{
if ($this->total_episodes === 0) return 0;
return (int) round($this->done_episodes / $this->total_episodes * 100);
}
public function getStatusColorAttribute(): string
{
return match($this->status) {
'pending' => 'secondary',
'fetching' => 'info',
'downloading' => 'primary',
'uploading' => 'warning',
'done' => 'success',
'failed' => 'danger',
default => 'secondary',
};
}
public function getStatusLabelAttribute(): string
{
return match($this->status) {
'pending' => 'Bekliyor',
'fetching' => 'Bölümler Alınıyor',
'downloading' => 'İndiriliyor',
'uploading' => 'Yükleniyor',
'done' => 'Tamamlandı',
'failed' => 'Hata',
default => $this->status,
};
}
/** Toplam bölüm sayısını season_ranges'ten hesapla */
public function buildEpisodeList(): array
{
if (!$this->season_ranges) return [];
$episodes = [];
foreach ($this->season_ranges as $range) {
$season = (int) $range['season'];
$from = (int) $range['from'];
$to = (int) $range['to'];
for ($ep = $from; $ep <= $to; $ep++) {
$episodes[] = ['season' => $season, 'episode' => $ep];
}
}
return $episodes;
}
public function getTotalFromRangesAttribute(): int
{
$total = 0;
foreach (($this->season_ranges ?? []) as $r) {
$total += max(0, (int)$r['to'] - (int)$r['from'] + 1);
}
return $total;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MembershipPlan extends Model
{
protected $fillable = [
'name', 'slug', 'description', 'price', 'purchase_link', 'duration_days', 'trial_days',
'features', 'perks', 'is_active', 'is_public', 'visible_until', 'sort_order',
'badge_label', 'accent_color',
];
protected $casts = [
'features' => 'array',
'perks' => 'array',
'is_active' => 'boolean',
'is_public' => 'boolean',
'visible_until' => 'datetime',
'price' => 'float',
'trial_days' => 'integer',
];
/** Belirli bir perk'in bu planda aktif olup olmadığını döner */
public function hasPerk(string $key): bool
{
return !empty(($this->perks ?? [])[$key]);
}
public function subscriptions()
{
return $this->hasMany(Subscription::class, 'plan_id');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
public $timestamps = false;
protected $fillable = ['conversation_id', 'user_id', 'body'];
protected $casts = ['created_at' => 'datetime'];
public function user() { return $this->belongsTo(User::class); }
public function conversation() { return $this->belongsTo(Conversation::class); }
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ModeratorPermission extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'permission', 'granted_by'];
// Permission groups and their keys — single source of truth
public static array $groups = [
'Anime Yönetimi' => [
'animes.view' => 'Anime listesini görüntüle',
'animes.create' => 'Yeni anime ekle',
'animes.edit' => 'Anime düzenle',
'animes.delete' => 'Anime sil',
'animes.publish' => 'Anime yayınla / gizle',
],
'Bölüm Yönetimi' => [
'episodes.view' => 'Bölümleri görüntüle',
'episodes.create' => 'Bölüm ekle',
'episodes.edit' => 'Bölüm düzenle',
'episodes.delete' => 'Bölüm sil',
],
'Kullanıcı Yönetimi' => [
'users.view' => 'Kullanıcıları görüntüle',
'users.edit' => 'Kullanıcı bilgilerini düzenle',
'users.ban' => 'Kullanıcı banla / ban kaldır',
'users.premium' => 'Premium ver / al',
],
'Yorum Yönetimi' => [
'comments.view' => 'Yorumları görüntüle',
'comments.approve' => 'Yorum onayla / reddet',
'comments.delete' => 'Yorum sil',
'comments.pin' => 'Yorum sabitle',
],
'İçerik Yönetimi' => [
'genres.manage' => 'Türleri yönet',
'banners.manage' => 'Bannerleri yönet',
'requests.manage' => 'Anime isteklerini yönet',
'tribunal.manage' => 'Mahkeme yönet',
'import.manage' => 'Anime import et',
],
'Analitik & Raporlar' => [
'analytics.view' => 'Analitikleri görüntüle',
'analytics.bots' => 'Bot analitiğini görüntüle',
'analytics.block' => 'IP engelle / engel kaldır',
'analytics.users' => 'Kullanıcı analitiği & aktivite',
],
'Bildirimler' => [
'notifications.send' => 'Push bildirim gönder',
'notifications.view' => 'Bildirim geçmişini görüntüle',
],
];
public static function allKeys(): array
{
return collect(self::$groups)->flatMap(fn($g) => array_keys($g))->values()->all();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function grantedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'granted_by');
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
protected $fillable = [
'user_id', 'plan_id', 'conversation_id', 'token',
'amount', 'status', 'iyzico_payment_id', 'error_message', 'paid_at',
];
protected $casts = [
'amount' => 'decimal:2',
'paid_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function plan()
{
return $this->belongsTo(MembershipPlan::class, 'plan_id');
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PermissionSetting extends Model
{
protected $fillable = ['key', 'label', 'required_membership', 'description'];
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PredictionVote extends Model
{
public $timestamps = false;
protected $fillable = ['prediction_id', 'user_id'];
public function prediction() { return $this->belongsTo(EpisodePrediction::class, 'prediction_id'); }
public function user() { return $this->belongsTo(User::class); }
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use App\Support\MediaUrl;
use Illuminate\Database\Eloquent\Model;
class Season extends Model
{
protected $fillable = [
'anime_id', 'season_number', 'mal_id', 'title', 'description',
'cover_image', 'release_year', 'is_published',
];
protected $casts = ['is_published' => 'boolean'];
public function anime()
{
return $this->belongsTo(Anime::class);
}
public function episodes()
{
return $this->hasMany(Episode::class)->orderBy('episode_number');
}
public function getCoverUrlAttribute(): ?string
{
return MediaUrl::fromStoragePath($this->cover_image);
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SeoKeyword extends Model
{
protected $table = 'seo_keyword_tracker';
protected $fillable = [
'keyword', 'target_url', 'search_volume', 'difficulty', 'notes',
];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SeoRedirect extends Model
{
protected $table = 'seo_redirects';
protected $fillable = [
'from_path', 'to_path', 'type', 'hits', 'is_active',
];
protected $casts = ['is_active' => 'boolean'];
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
protected $fillable = ['key', 'value', 'group'];
public static function get(string $key, $default = null)
{
return static::where('key', $key)->value('value') ?? $default;
}
public static function set(string $key, $value, string $group = 'general'): void
{
static::updateOrCreate(['key' => $key], ['value' => $value, 'group' => $group]);
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SpoilerBox extends Model
{
protected $fillable = ['episode_id', 'user_id', 'body', 'is_spoiler', 'spoiler_score', 'likes'];
protected $casts = [
'is_spoiler' => 'boolean',
];
public function episode() { return $this->belongsTo(Episode::class); }
public function user() { return $this->belongsTo(User::class); }
public function boxLikes(){ return $this->hasMany(SpoilerBoxLike::class, 'box_id'); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SpoilerBoxLike extends Model
{
public $timestamps = false;
protected $fillable = ['box_id', 'user_id', 'created_at'];
public function box() { return $this->belongsTo(SpoilerBox::class, 'box_id'); }
public function user() { return $this->belongsTo(User::class); }
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Subscription extends Model
{
protected $fillable = [
'user_id', 'plan_id', 'status', 'starts_at', 'expires_at',
'payment_method', 'payment_ref', 'notes',
];
protected $casts = [
'starts_at' => 'datetime',
'expires_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function plan()
{
return $this->belongsTo(MembershipPlan::class, 'plan_id');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Subtitle extends Model
{
protected $fillable = ['episode_id', 'language', 'label', 'url', 'is_default'];
public function episode()
{
return $this->belongsTo(Episode::class);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TimeCapsule extends Model
{
protected $fillable = ['user_id', 'anime_id', 'message', 'unlock_at', 'opened_at'];
protected $casts = [
'unlock_at' => 'datetime',
'opened_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
public function isUnlocked(): bool
{
return now()->gte($this->unlock_at);
}
public function isOpened(): bool
{
return !is_null($this->opened_at);
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tribunal extends Model
{
protected $fillable = [
'anime_id', 'episode_id', 'created_by', 'question',
'side_a', 'side_b', 'extra_sides', 'status', 'verdict', 'closes_at',
];
protected $casts = [
'closes_at' => 'datetime',
'extra_sides' => 'array',
];
// Tüm tarafları ['a'=>'Haklıydı', 'b'=>'Haksızdı', 'c'=>'...'] formatında döndür
public function allSides(): array
{
$sides = ['a' => $this->side_a, 'b' => $this->side_b];
foreach (($this->extra_sides ?? []) as $i => $label) {
$sides[chr(99 + $i)] = $label; // c, d, e, ...
}
return $sides;
}
public function anime() { return $this->belongsTo(Anime::class); }
public function episode() { return $this->belongsTo(Episode::class); }
public function creator() { return $this->belongsTo(User::class, 'created_by'); }
public function votes() { return $this->hasMany(TribunalVote::class); }
public function arguments() { return $this->hasMany(TribunalArgument::class); }
public function voteCountA() { return $this->votes()->where('side', 'a')->count(); }
public function voteCountB() { return $this->votes()->where('side', 'b')->count(); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TribunalArgument extends Model
{
protected $fillable = ['tribunal_id', 'user_id', 'side', 'body', 'vote_count'];
public function tribunal() { return $this->belongsTo(Tribunal::class); }
public function user() { return $this->belongsTo(User::class); }
public function argVotes() { return $this->hasMany(TribunalArgumentVote::class, 'argument_id'); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TribunalArgumentVote extends Model
{
public $timestamps = false;
protected $fillable = ['argument_id', 'user_id', 'created_at'];
public function argument() { return $this->belongsTo(TribunalArgument::class); }
public function user() { return $this->belongsTo(User::class); }
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TribunalVote extends Model
{
public $timestamps = false;
protected $fillable = ['tribunal_id', 'user_id', 'side', 'created_at'];
public function tribunal() { return $this->belongsTo(Tribunal::class); }
public function user() { return $this->belongsTo(User::class); }
}
+264
View File
@@ -0,0 +1,264 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
protected $fillable = [
'name', 'username', 'email', 'password', 'avatar',
'social_provider', 'social_id',
'bio', 'banner_image', 'website', 'twitter', 'instagram', 'discord',
'profile_color', 'show_watchlist', 'show_activity',
'role', 'membership', 'premium_expires_at',
'is_banned', 'ban_reason', 'banned_at',
'gif_avatar', 'comment_bg', 'comment_glow', 'username_color', 'username_effect',
'profile_frame', 'profile_badge', 'admin_badge', 'profile_bg', 'profile_theme',
'profile_music_url', 'comment_signature', 'entry_effect', 'animated_banner',
];
protected $hidden = ['password', 'remember_token'];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'premium_expires_at' => 'datetime',
'banned_at' => 'datetime',
'is_banned' => 'boolean',
'show_watchlist' => 'boolean',
'show_activity' => 'boolean',
'animated_banner' => 'boolean',
'password' => 'hashed',
];
}
/**
* /u/{username} gibi URL'lerde username veya ID ile çözümleme.
* custom_profile_url perki olan kullanıcılar /u/kullanici-adi şeklinde erişilebilir.
*/
public function resolveRouteBinding($value, $field = null): ?self
{
if (is_numeric($value)) {
return static::find($value);
}
return static::where('username', $value)
->whereNotNull('username')
->first();
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isModerator(): bool
{
return in_array($this->role, ['admin', 'moderator']);
}
public function moderatorPermissions()
{
return $this->hasMany(ModeratorPermission::class);
}
public function activityLogs()
{
return $this->hasMany(UserActivityLog::class);
}
/** Returns cached permission set for this user. Admins have all permissions. */
public function can_mod(string $permission): bool
{
if ($this->isAdmin()) return true;
if ($this->role !== 'moderator') return false;
$key = "mod_perms_{$this->id}";
$perms = cache()->remember($key, 300, fn() =>
ModeratorPermission::where('user_id', $this->id)->pluck('permission')->all()
);
return in_array($permission, $perms);
}
/** Flush cached permissions (call after saving changes). */
public function flushPermCache(): void
{
cache()->forget("mod_perms_{$this->id}");
}
/** isPremium() sonucunu istek başına önbellekle — sayfa başına onlarca kez çağrılıyor */
private ?bool $_isPremiumCache = null;
public function isPremium(): bool
{
if ($this->_isPremiumCache !== null) {
return $this->_isPremiumCache;
}
// 1. yol: membership alanı 'premium' ve süresi dolmamış
$viaMembership = $this->membership === 'premium'
&& ($this->premium_expires_at === null || $this->premium_expires_at->isFuture());
if ($viaMembership) {
return $this->_isPremiumCache = true;
}
// 2. yol: aktif abonelik var ama membership alanı senkronize değil
// (2 farklı premium yolu — biri güncellenmezse kullanıcı yine premium sayılır)
try {
$viaSubscription = $this->subscriptions()
->where('status', 'active')
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->exists();
} catch (\Throwable) {
$viaSubscription = false;
}
return $this->_isPremiumCache = $viaSubscription;
}
/**
* Kullanıcının aktif planında belirli bir perk var mı?
* Ücretsiz kullanıcılarda her zaman false döner.
*/
public function hasPerk(string $key): bool
{
if (!$this->isPremium()) return false;
// Ücretsiz mod: tüm perkler herkese açık
if (self::freeModeActive()) return true;
$sub = $this->subscriptions()
->where('status', 'active')
->where(fn($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->latest()
->with('plan')
->first();
if (!$sub?->plan) return false;
$perks = $sub->plan->perks ?? [];
return !empty($perks[$key]);
}
/** premium_free_mode ayarını 5dk cache'leyerek okur */
public static function freeModeActive(): bool
{
return cache()->remember('premium_free_mode', 300, fn() =>
\App\Models\Setting::get('premium_free_mode', '0')
) === '1';
}
/**
* Kullanıcının aktif avatar URL'si: GIF avatar varsa ve hasPerk('gif_avatar') ise döner.
*/
public function effectiveAvatar(): ?string
{
if ($this->gif_avatar && $this->hasPerk('gif_avatar')) {
return $this->gif_avatar;
}
return $this->avatar;
}
public function subscriptions()
{
return $this->hasMany(Subscription::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
// ── Social ────────────────────────────────────────────────────────────────
public function followers()
{
return $this->belongsToMany(User::class, 'user_follows', 'following_id', 'follower_id')
->withPivot('created_at');
}
public function following()
{
return $this->belongsToMany(User::class, 'user_follows', 'follower_id', 'following_id')
->withPivot('created_at');
}
public function isFollowing(int $userId): bool
{
return \App\Models\UserFollow::where('follower_id', $this->id)
->where('following_id', $userId)
->exists();
}
public function conversations()
{
return $this->belongsToMany(Conversation::class, 'conversation_participants')
->withPivot('last_read_at');
}
public function totalUnreadMessages(): int
{
return $this->conversations()
->with(['messages' => fn($q) => $q->where('user_id', '!=', $this->id)])
->get()
->sum(fn($c) => $c->unreadCountFor($this->id));
}
// Anime zevk uyum skoru (0-100)
public function compatibilityWith(User $other): int
{
$myIds = \App\Models\Watchlist::where('user_id', $this->id)->pluck('anime_id');
$theirIds = \App\Models\Watchlist::where('user_id', $other->id)->pluck('anime_id');
if ($myIds->isEmpty() || $theirIds->isEmpty()) return 0;
$mySet = $myIds->unique()->values();
$theirSet = $theirIds->unique()->values();
$intersection = $mySet->intersect($theirSet)->count();
$union = $mySet->merge($theirSet)->unique()->count();
$jaccard = $union > 0 ? $intersection / $union : 0;
// Rating similarity bonus
$myRatings = \DB::table('anime_ratings')->where('user_id', $this->id)->pluck('rating', 'anime_id');
$theirRatings = \DB::table('anime_ratings')->where('user_id', $other->id)->pluck('rating', 'anime_id');
$commonAnimes = $myRatings->keys()->intersect($theirRatings->keys());
$ratingScore = 0;
if ($commonAnimes->count() > 0) {
$diffs = $commonAnimes->map(fn($id) => abs($myRatings[$id] - $theirRatings[$id]) / 10);
$ratingScore = 1 - $diffs->avg();
}
$score = $jaccard * 0.6 + $ratingScore * 0.4;
return (int) round(min($score * 100, 100));
}
/**
* İzleme saatine göre rank bilgisi döner.
* watch_rank perki yoksa null döner.
*/
public function watchRank(): ?array
{
if (!$this->hasPerk('watch_rank')) return null;
$totalSeconds = \App\Models\ContinueWatching::where('user_id', $this->id)
->sum('seconds_watched');
$hours = $totalSeconds / 3600;
return match(true) {
$hours >= 500 => ['label' => 'Efsane', 'color' => '#ff2d7d', 'icon' => 'bi-trophy-fill'],
$hours >= 250 => ['label' => 'Usta', 'color' => '#ffd700', 'icon' => 'bi-star-fill'],
$hours >= 100 => ['label' => 'Bağımlı', 'color' => '#b84dff', 'icon' => 'bi-heart-fill'],
$hours >= 50 => ['label' => 'Hayran', 'color' => '#00f5ff', 'icon' => 'bi-eye-fill'],
$hours >= 20 => ['label' => 'İzleyici', 'color' => '#00d4a4', 'icon' => 'bi-play-circle-fill'],
default => ['label' => 'Acemi', 'color' => '#9ca3af', 'icon' => 'bi-controller'],
};
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UserAchievement extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'achievement_id', 'earned_at'];
protected $casts = ['earned_at' => 'datetime'];
public function user() { return $this->belongsTo(User::class); }
public function achievement() { return $this->belongsTo(Achievement::class); }
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserActivityLog extends Model
{
public $timestamps = false;
protected $fillable = [
'user_id', 'session_id', 'action', 'subject_type', 'subject_id',
'ip', 'country', 'city', 'device', 'browser', 'user_agent', 'is_bot', 'meta',
];
protected $casts = ['meta' => 'array', 'is_bot' => 'boolean'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
// Human-readable action labels
public static array $actionLabels = [
'login' => 'Giriş',
'logout' => 'Çıkış',
'register' => 'Kayıt',
'pageview' => 'Sayfa Görüntüleme',
'anime_view' => 'Anime Görüntüleme',
'episode_watch' => 'Bölüm İzleme',
'comment_create' => 'Yorum',
'watchlist_add' => 'Listeye Ekle',
'watchlist_remove'=> 'Listeden Çıkar',
'rating' => 'Puan Verdi',
'follow' => 'Takip',
'search' => 'Arama',
'download' => 'İndirme',
'capsule_create' => 'Kapsül Oluşturdu',
'tribunal_vote' => 'Mahkeme Oyu',
'prediction_vote' => 'Tahmin Oyu',
'nico_comment' => 'Nico Yorum',
'password_change' => 'Şifre Değiştirdi',
'profile_update' => 'Profil Güncelledi',
];
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UserFollow extends Model
{
public $timestamps = false;
protected $fillable = ['follower_id', 'following_id'];
public function follower() { return $this->belongsTo(User::class, 'follower_id'); }
public function following() { return $this->belongsTo(User::class, 'following_id'); }
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UserNotification extends Model
{
public $timestamps = false;
protected $fillable = ['user_id', 'type', 'data', 'read_at'];
protected $casts = [
'data' => 'array',
'read_at' => 'datetime',
'created_at' => 'datetime',
];
public function user() { return $this->belongsTo(User::class); }
public function getIsReadAttribute(): bool
{
return $this->read_at !== null;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class VideoSource extends Model
{
protected $fillable = [
'episode_id', 'label', 'url', 'type', 'quality',
'translator_id', 'sort_order', 'is_default', 'source',
'is_hevc', 'hevc_checked_at',
];
protected $casts = [
'is_default' => 'boolean',
'is_hevc' => 'boolean',
'hevc_checked_at' => 'datetime',
];
public function episode()
{
return $this->belongsTo(Episode::class);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VoiceCall extends Model
{
protected $fillable = [
'caller_id', 'callee_id', 'channel_name', 'status', 'answered_at', 'ended_at',
];
protected $casts = [
'answered_at' => 'datetime',
'ended_at' => 'datetime',
];
public function caller(): BelongsTo
{
return $this->belongsTo(User::class, 'caller_id');
}
public function callee(): BelongsTo
{
return $this->belongsTo(User::class, 'callee_id');
}
public function isActive(): bool
{
return in_array($this->status, ['ringing', 'active']);
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class WatchParty extends Model
{
protected $fillable = [
'room_code', 'host_user_id', 'episode_id',
'current_sec', 'is_playing', 'synced_at',
'max_members', 'is_private', 'password',
];
protected $casts = [
'is_playing' => 'boolean',
'is_private' => 'boolean',
'current_sec'=> 'integer',
'synced_at' => 'datetime',
];
public function host() { return $this->belongsTo(User::class, 'host_user_id'); }
public function episode() { return $this->belongsTo(Episode::class); }
public function members() { return $this->hasMany(WatchPartyMember::class, 'party_id'); }
public function activeMembers()
{
return $this->members()->where('last_ping', '>=', now()->subSeconds(30));
}
public static function generateCode(): string
{
do {
$code = strtoupper(Str::random(6));
} while (self::where('room_code', $code)->exists());
return $code;
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class WatchPartyMember extends Model
{
public $timestamps = false;
protected $fillable = ['party_id', 'user_id', 'joined_at', 'last_ping'];
protected $casts = ['joined_at' => 'datetime', 'last_ping' => 'datetime'];
public function party() { return $this->belongsTo(WatchParty::class, 'party_id'); }
public function user() { return $this->belongsTo(User::class); }
}
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Watchlist extends Model
{
protected $fillable = ['user_id', 'anime_id', 'status'];
protected $casts = ['created_at' => 'datetime', 'updated_at' => 'datetime'];
const STATUSES = [
'plan' => 'İzlenecek',
'watching' => 'İzleniyor',
'completed' => 'Tamamlandı',
'dropped' => 'Bırakıldı',
];
public function user() { return $this->belongsTo(User::class); }
public function anime() { return $this->belongsTo(Anime::class); }
}