Add NAS file manager integration and all pending platform changes
- Installed p7h/nas-file-manager package via private VCS repo - Published config/nas-file-manager.php with super_admin middleware restriction - Added NAS env vars to .env.example - Created admin/nas-storage page with connection info panel and file browser widget - Added NAS Storage link to admin sidebar (super_admin only) - Added SuperAdminController@nasStorage method and admin.nas-storage route - Includes all accumulated branch changes: profile wall, 2FA, audit logs, settings panel, country/phone/timezone components, posts, slideshow, playlist shares, video downloads/shares, comment likes, notifications, social links, and more Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
d44490dfe0
commit
0b2e95ea65
161 files changed
+27496
-7250
No files matched your search
+99
-4
@@ -25,6 +25,11 @@ class Video extends Model
|
||||
'is_shorts',
|
||||
'has_hls',
|
||||
'hls_path',
|
||||
'download_access',
|
||||
'download_count',
|
||||
'share_count',
|
||||
'share_token',
|
||||
'slideshow_video_path',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -54,6 +59,16 @@ class Video extends Model
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function slides()
|
||||
{
|
||||
return $this->hasMany(VideoSlide::class)->orderBy('position');
|
||||
}
|
||||
|
||||
public function hasSlideshow(): bool
|
||||
{
|
||||
return $this->slides()->count() > 1;
|
||||
}
|
||||
|
||||
// Accessors
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
@@ -92,10 +107,84 @@ class Video extends Model
|
||||
return \DB::table('video_views')->where('video_id', $this->id)->count();
|
||||
}
|
||||
|
||||
// Get shareable URL for the video
|
||||
// ── Short opaque URL encoding ─────────────────────────────────────
|
||||
// 3-round Feistel cipher over 30-bit space → base62.
|
||||
// Consecutive IDs produce completely different output. Pure PHP, no extensions.
|
||||
private const URL_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
private static function feistelRound(int $n, int $round): int
|
||||
{
|
||||
// Deterministic pseudo-random function per round (no extension needed)
|
||||
$keys = [0x2D4F8A1B, 0x7C3E6912, 0xA5B2D083];
|
||||
return (int)(($n * $keys[$round % 3] + ($round * 0x9E3779B9)) & 0x7FFF);
|
||||
}
|
||||
|
||||
public static function encodeId(int $id): string
|
||||
{
|
||||
// Split 30-bit id into two 15-bit halves
|
||||
$L = ($id >> 15) & 0x7FFF;
|
||||
$R = $id & 0x7FFF;
|
||||
// 3 Feistel rounds
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$tmp = $L ^ self::feistelRound($R, $i);
|
||||
$L = $R;
|
||||
$R = $tmp;
|
||||
}
|
||||
$n = (($L & 0x7FFF) << 15) | ($R & 0x7FFF);
|
||||
$base = strlen(self::URL_ALPHABET);
|
||||
$out = '';
|
||||
do {
|
||||
$out = self::URL_ALPHABET[$n % $base] . $out;
|
||||
$n = intdiv($n, $base);
|
||||
} while ($n > 0);
|
||||
// Pad to fixed length so all URLs look uniform
|
||||
return str_pad($out, 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
public static function decodeId(string $encoded): ?int
|
||||
{
|
||||
$base = strlen(self::URL_ALPHABET);
|
||||
$n = 0;
|
||||
for ($i = 0, $len = strlen($encoded); $i < $len; $i++) {
|
||||
$pos = strpos(self::URL_ALPHABET, $encoded[$i]);
|
||||
if ($pos === false) return null;
|
||||
$n = $n * $base + $pos;
|
||||
}
|
||||
// Reverse Feistel
|
||||
$L = ($n >> 15) & 0x7FFF;
|
||||
$R = $n & 0x7FFF;
|
||||
for ($i = 2; $i >= 0; $i--) {
|
||||
$tmp = $R ^ self::feistelRound($L, $i);
|
||||
$R = $L;
|
||||
$L = $tmp;
|
||||
}
|
||||
return (($L & 0x7FFF) << 15) | ($R & 0x7FFF);
|
||||
}
|
||||
|
||||
// Route model binding — use encoded ID in URLs
|
||||
public function getRouteKey()
|
||||
{
|
||||
return self::encodeId($this->id);
|
||||
}
|
||||
|
||||
public function getRouteKeyName()
|
||||
{
|
||||
return 'id';
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null)
|
||||
{
|
||||
$realId = self::decodeId((string) $value);
|
||||
if ($realId === null || $realId <= 0) {
|
||||
abort(404);
|
||||
}
|
||||
return $this->where('id', $realId)->firstOrFail();
|
||||
}
|
||||
|
||||
// All share URLs use the unguessable token route
|
||||
public function getShareUrlAttribute()
|
||||
{
|
||||
return route('videos.show', $this->id);
|
||||
return route('videos.showByToken', $this->share_token);
|
||||
}
|
||||
|
||||
// Get formatted duration (e.g., "1:30" or "0:45" for shorts)
|
||||
@@ -213,6 +302,12 @@ class Video extends Model
|
||||
return $this->type === 'match';
|
||||
}
|
||||
|
||||
public function isAudioOnly(): bool
|
||||
{
|
||||
$ext = strtolower(pathinfo($this->filename ?? '', PATHINFO_EXTENSION));
|
||||
return in_array($ext, ['mp3', 'm4a', 'aac', 'flac', 'wav']);
|
||||
}
|
||||
|
||||
// Shorts helpers
|
||||
public function isShorts()
|
||||
{
|
||||
@@ -331,13 +426,13 @@ class Video extends Model
|
||||
// Get video stream URL for Open Graph
|
||||
public function getStreamUrlAttribute()
|
||||
{
|
||||
return route('videos.stream', $this->id);
|
||||
return route('videos.stream', $this);
|
||||
}
|
||||
|
||||
// Get secure share URL
|
||||
public function getSecureShareUrlAttribute()
|
||||
{
|
||||
return secure_url(route('videos.show', $this->id));
|
||||
return secure_url(route('videos.show', $this));
|
||||
}
|
||||
|
||||
// Get secure thumbnail URL
|
||||
|
||||
Reference in new issue
Block a user