Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PermissionSetting extends Model
|
||||
{
|
||||
protected $fillable = ['key', 'label', 'required_membership', 'description'];
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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'); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(); }
|
||||
}
|
||||
@@ -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'); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
@@ -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'); }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
Reference in New Issue
Block a user