Compare commits

93 Commits

Author SHA1 Message Date
w33b f7c37df755 Show subtitles toggle & menu 2026-07-29 22:12:51 +02:00
w33b 6a9d3b25bf Add styling to plyr and fix switcher not hiding 2026-07-28 21:54:50 +02:00
w33b 75b98de746 Add plyr again as fallback 2026-07-28 21:41:30 +02:00
w33b eaf48276f0 Update player setting design 2026-07-28 20:33:20 +02:00
w33b 2e3918def4 Custom video player 2026-07-26 22:26:37 +02:00
w33b 7553b9f895 Update stats page design 2026-07-25 14:18:50 +02:00
w33b 5af1c3c447 Update profile pages design 2026-07-25 13:54:21 +02:00
w33b 2b1a967065 Update nav live search design 2026-07-25 12:38:00 +02:00
w33b ee1c17b903 Remove unused function 2026-07-19 20:31:43 +02:00
w33b a5bf2ac245 Add video engagement tracking and heatmap 2026-07-19 20:13:55 +02:00
w33b 4f81164b44 Only track was watched if more than 10s is played #6 2026-07-19 16:10:24 +02:00
w33b 859a35847a Improve admin user & comment moderation page 2026-07-19 15:10:20 +02:00
w33b f35d1a119e Add ability to edit the playlist name and visibility #5 2026-07-19 13:22:28 +02:00
w33b a04d58c60f Remove external subscription system 2026-07-18 14:03:03 +02:00
w33b bcd15d8569 Discord socialite: catch InvalidStateException 2026-06-15 21:14:35 +02:00
w33b 133a069890 Fix attempt to read property on null on playlist page 2026-06-15 20:51:07 +02:00
w33b b32465a7d0 Fix social preview & SEO optimization 2026-06-14 14:07:10 +02:00
w33b f8022b3f18 Use CSS classes to hide n-th elements instead of JS 2026-05-27 17:12:13 +02:00
w33b 57d1ec34c3 Show two covers side by side on mobile 2026-05-26 16:14:29 +02:00
w33b 81639aaabf Use laravel task scheduler 2026-05-26 15:17:57 +02:00
w33b 5dc1bff60c Add an indicator for v2 releases in the download popup #7 2026-05-26 14:36:21 +02:00
w33b 2f3f0edc30 Add ability to edit hentai title & Update design of edit modal 2026-05-26 14:18:48 +02:00
w33b a71b2976af Add ability for mods to edit episode & Refactor code 2026-05-26 13:45:13 +02:00
w33b 2c016274ab Update packages 2026-05-26 12:06:54 +02:00
w33b 5ba0a55316 Update series page design & Refactor 2026-05-26 12:01:36 +02:00
w33b a6fe34a0d1 Reduce queries on home page by eager loading into cache 2026-05-24 22:15:16 +02:00
w33b bb53e06c69 Smaller style fix 2026-05-24 21:39:46 +02:00
w33b 5cae5dc658 Fix more responsiveness issues 2026-05-24 19:39:20 +02:00
w33b 356d07365f Update episode card design 2026-05-24 19:38:50 +02:00
w33b 3574d20fae Update hentai info design & Remove livewire view count 2026-05-24 15:52:22 +02:00
w33b 9fc9e8ed10 Update comments design 2026-05-24 14:49:46 +02:00
w33b f5c706b587 Fix smaller design issues 2026-05-24 12:14:19 +02:00
w33b cbea71d9ae Make home more responsive depending on screen width 2026-05-24 12:06:44 +02:00
w33b 64a621173c Fix hover to preview once and for all 2026-05-23 14:48:25 +02:00
w33b 839779b82e Update cover/thumbnail design 2026-05-23 14:43:35 +02:00
w33b 112cf9433e Make background darker 2026-05-23 14:00:11 +02:00
w33b 26a6500fca Remove unused modals 2026-05-23 11:46:30 +02:00
w33b d8cf70e747 Update more modal designs 2026-05-23 11:46:20 +02:00
w33b 0d4545c2ab Update language selector modal design 2026-05-23 11:00:18 +02:00
w33b 900103e1c2 Fix setting locale for guest users 2026-05-23 10:59:30 +02:00
w33b d4c90976f8 Fix janky loading icon & weird offset of modal 2026-05-23 10:29:07 +02:00
w33b 72263127df Update filter modal designs & Refactor code 2026-05-23 00:01:02 +02:00
w33b 6d3de59929 Update search filter design 2026-05-22 23:20:00 +02:00
w33b ddb1bc2d14 Stream page sidebar fix lightmode 2026-05-22 23:13:00 +02:00
w33b 5f3874a233 Stream page sidebar make padding consistent 2026-05-22 23:10:41 +02:00
w33b ba3650899e Refactor home template 2026-05-22 23:07:19 +02:00
w33b 904604fcfb Update hover color of tabs on home page 2026-05-22 22:44:38 +02:00
w33b b7b34b503c Update categories design 2026-05-22 22:43:57 +02:00
w33b 4928733383 Refactor by using app layout 2026-05-22 22:34:03 +02:00
w33b 6340302ac6 Update Cover & Thumbnail Design & Refactor 2026-05-22 22:16:31 +02:00
w33b c1829ba7bd Fix offset of thumbnail as guest 2026-05-22 20:59:45 +02:00
w33b 0b155bbb80 Update new comments design 2026-05-22 20:34:06 +02:00
w33b 9f959efa14 Update footer design 2026-05-22 20:31:38 +02:00
w33b 38e3346dc3 Update stats design 2026-05-22 19:41:07 +02:00
w33b 09c08f3fea Add comment restore button 2026-05-06 21:15:15 +02:00
w33b 75f631c3e6 Add MogLog System 2026-05-06 21:08:51 +02:00
w33b fdf26604f3 Add ability to delete comments by moderators 2026-05-06 19:02:50 +02:00
w33b 59cb39ca77 Fix remove role function 2026-05-06 16:27:42 +02:00
w33b 62647be75c Fix active status not showing immediatly (livewire) 2026-05-05 16:02:00 +02:00
w33b de6efb877c Add job to sync subscription keys 2026-05-04 20:22:51 +02:00
w33b 2151d69791 Add external subscription system 2026-05-04 19:12:21 +02:00
w33b 05d4ef1bdb Add Passkey Support & Pint 2026-04-21 15:56:46 +02:00
w33b 8ae9eaaadb Replace captcha package with own implementation 2026-04-20 21:36:02 +02:00
w33b 361b511c3e Update npm packages 2026-04-18 19:17:30 +02:00
w33b 1bc505057f Fix adding theme switch event listener on login page 2026-04-18 19:15:43 +02:00
w33b a78b1c41ac Remove unused code & Move files to correct folder 2026-04-18 18:42:09 +02:00
w33b 2480c5b309 Redo user comments list 2026-04-18 16:28:50 +02:00
w33b 4fc11d7329 Redo notification list 2026-04-18 15:47:06 +02:00
w33b f3e5100d5d Pint 2026-04-18 14:18:52 +02:00
w33b 5b4d3d435e Display the correct file extension on download button 2026-03-06 22:06:20 +01:00
w33b 564f816fb9 Add matrix nsfw rooms 2026-03-04 14:34:16 +01:00
w33b 3709e378c3 Fix Admin Download Buttons 2026-03-03 21:17:43 +01:00
w33b 4a45dae593 Update Readme 2026-03-01 16:07:09 +01:00
w33b 2b0448d517 Add Disclaimer and Guide for Matrix 2026-03-01 16:06:58 +01:00
w33b 3bb6af73c3 Add matrix 2026-02-18 12:34:10 +01:00
w33b 57cf153560 Update Livewire to fix CorruptComponentPayloadException 2026-02-15 14:52:16 +01:00
w33b e45fd4b148 Update axios 2026-02-15 13:02:08 +01:00
w33b d479369770 Merge branch 'laravel-12' 2026-01-18 18:54:25 +01:00
w33b af739e3c88 Remove old comments repo in composer.json 2026-01-18 18:49:59 +01:00
w33b 273ed65a8d Remove laravel sail 2026-01-18 18:44:59 +01:00
w33b ccfd5b996b Replace captcha system 2026-01-18 18:37:08 +01:00
w33b e5ef197ed6 Add user roles system 2026-01-16 23:14:47 +01:00
w33b c0be2e294a Refactor routes 2026-01-16 23:01:34 +01:00
w33b b8ba17b33f Fix patreon role check (again again) 2026-01-16 15:18:29 +00:00
w33b 5a8dd12cb8 Fix patreon role check (again)
Life without typecast is hard
2026-01-16 15:11:28 +00:00
w33b 3a77c4320d Fix patreon role check
We compare the roles using a strict type check, the json contents from the discord api are strings and not integers.
2026-01-16 14:42:39 +00:00
w33b 823a284fbc Fix PHP 8.4 deprecation warning 2026-01-11 16:39:14 +01:00
w33b 67e601d0c4 Add ability to properly set locale (session/account) 2026-01-11 16:33:13 +01:00
w33b 7e4ebd91ad Remove vluzrmos/language-detector package 2026-01-11 16:31:16 +01:00
w33b 4dc5dee2b9 Update dependencies 2026-01-11 15:46:33 +01:00
w33b 5310908b0c Update login button on mobile 2026-01-11 00:21:05 +01:00
w33b 4b05b3db6d Fix cache not flushed after comment delete by admin 2026-01-10 23:24:31 +01:00
w33b df47a926e4 Fix comment mass delete 2026-01-10 23:21:29 +01:00
282 changed files with 15189 additions and 6825 deletions
+7 -7
View File
@@ -2,13 +2,13 @@
## hstream Website
### Install
### Install (Ubuntu)
```bash
# Install PHP
sudo add-apt-repository ppa:ondrej/php
apt update && apt upgrade
apt install php8.3 php8.3-xml php8.3-mysql php8.3-gd php8.3-zip php8.3-curl php8.3-mbstring
apt install php8.4 php8.4-xml php8.4-mysql php8.4-gd php8.4-zip php8.4-curl php8.4-mbstring
# Install NodeJS
curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
@@ -22,7 +22,7 @@ mv composer.phar composer
# Install NGINX (skip for local dev)
apt install nginx
apt install php8.3-fpm
apt install php8.4-fpm
# Install MariaDB
apt install mariadb-server
@@ -54,7 +54,7 @@ nano /etc/supervisor/conf.d/laravel-queue.conf :
[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
command=php84 /var/www/hstream/artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
@@ -83,9 +83,9 @@ zip -r hstream_2023_11_30.zip hstream/
### Update
```bash
php artisan down
php84 artisan down
git pull
npm run build
php artisan view:clear && php artisan optimize:clear && php artisan cache:clear && service php8.4-fpm restart
php artisan up
php84 artisan view:clear && php84 artisan optimize:clear && php84 artisan cache:clear && service php8.4-fpm restart
php84 artisan up
```
+2 -3
View File
@@ -3,11 +3,10 @@
namespace App\Console\Commands;
use App\Models\PopularDaily;
use App\Models\PopularWeekly;
use App\Models\PopularMonthly;
use Illuminate\Support\Carbon;
use App\Models\PopularWeekly;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class AutoStats extends Command
{
+2 -3
View File
@@ -4,7 +4,6 @@ namespace App\Console\Commands;
use App\Models\Episode;
use App\Models\Hentai;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Spatie\Sitemap\Sitemap;
@@ -17,7 +16,7 @@ class GenerateSitemap extends Command
*
* @var string
*/
protected $signature = 'sitemap:generate';
protected $signature = 'app:generate-sitemap';
/**
* The console command description.
@@ -45,7 +44,7 @@ class GenerateSitemap extends Command
->setLastModificationDate(Carbon::create('2023', '8', '1')))
->add(Episode::all())
->add(Hentai::all());
$sitemap->writeToFile(public_path('sitemap.xml'));
}
}
+2 -3
View File
@@ -2,9 +2,8 @@
namespace App\Console\Commands;
use App\Models\Downloads;
use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads;
use Illuminate\Console\Command;
class GetFileSize extends Command
@@ -28,7 +27,7 @@ class GetFileSize extends Command
*/
public function handle()
{
foreach(Downloads::whereNull('size')->get() as $download) {
foreach (Downloads::whereNull('size')->get() as $download) {
GetFileSizeFromCDN::dispatch($download->id);
}
+1 -2
View File
@@ -4,9 +4,8 @@ namespace App\Console\Commands;
use App\Models\User;
use App\Models\UserDownload;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class ResetUserDownloads extends Command
{
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum UserRole: string
{
case ADMINISTRATOR = 'admin';
case MODERATOR = 'moderator';
case SUPPORTER = 'supporter';
case BANNED = 'banned';
}
+116 -19
View File
@@ -3,23 +3,24 @@
namespace App\Helpers;
use App\Models\Comment;
use App\Models\Downloads;
use App\Models\Episode;
use App\Models\Hentai;
use App\Models\PopularDaily;
use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use App\Models\User;
use Conner\Tagging\Model\Tag;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class CacheHelper
{
public static function getRecentlyReleased(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("recently_released_".$guestString, now()->addMinutes(60), function () use ($guest) {
return Cache::remember('recently_released_'.$guestString, now()->addMinutes(60), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('release_date', 'desc')
@@ -31,7 +32,8 @@ class CacheHelper
public static function getRecentlyUploaded(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("recently_uploaded".$guestString, now()->addMinutes(5), function () use ($guest) {
return Cache::remember('recently_uploaded'.$guestString, now()->addMinutes(5), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('created_at', 'desc')
@@ -42,32 +44,127 @@ class CacheHelper
public static function getTotalViewCount()
{
return Cache::remember("total_view_count", now()->addMinutes(60), function () {
return Cache::remember('total_view_count', now()->addMinutes(60), function () {
return Episode::sum('view_count');
});
}
public static function getTotalEpisodeCount()
{
return Cache::remember("total_episode_count", now()->addMinutes(60), function () {
return Cache::remember('total_episode_count', now()->addMinutes(60), function () {
return Episode::count();
});
}
public static function getTotalHentaiCount()
{
return Cache::remember("total_hentai_count", now()->addMinutes(60), function () {
return Cache::remember('total_hentai_count', now()->addMinutes(60), function () {
return Hentai::count();
});
}
public static function getTotalUserCount()
{
return Cache::remember('total_user_count', now()->addMinutes(60), function () {
return User::count();
});
}
public static function getTotalCommentCount()
{
return Cache::remember('total_comment_count', now()->addMinutes(60), function () {
return Comment::count();
});
}
public static function getTotalLikeCount()
{
return Cache::remember('total_like_count', now()->addMinutes(60), function () {
return DB::table('markable_likes')->count();
});
}
public static function getTotalDownloadCount()
{
return Cache::remember('total_download_count', now()->addMinutes(60), function () {
return Downloads::sum('count');
});
}
public static function get4kEpisodeCount()
{
return Cache::remember('episodes_4k_count', now()->addMinutes(60), function () {
return Episode::where('interpolated', true)->count();
});
}
public static function getUHD48FpsEpisodeCount()
{
return Cache::remember('episodes_uhd48_count', now()->addMinutes(60), function () {
return Episode::where('interpolated_uhd', true)->count();
});
}
public static function getTodayViewCount()
{
return Cache::remember('today_view_count', now()->addMinutes(30), function () {
return PopularDaily::whereDate('created_at', today())->count();
});
}
public static function getWeeklyViewCount()
{
return Cache::remember('weekly_view_count', now()->addMinutes(60), function () {
return PopularWeekly::whereDate('created_at', '>=', today()->subDays(7))->count();
});
}
public static function getPreviousWeeklyViewCount()
{
return Cache::remember('prev_weekly_view_count', now()->addMinutes(60), function () {
return PopularWeekly::whereDate('created_at', '>=', today()->subDays(14))
->whereDate('created_at', '<', today()->subDays(7))
->count();
});
}
public static function getAverageViewsPerEpisode()
{
return Cache::remember('avg_views_per_episode', now()->addMinutes(60), function () {
$totalEpisodes = Episode::count();
if ($totalEpisodes === 0) {
return 0;
}
return round(Episode::sum('view_count') / $totalEpisodes);
});
}
public static function getNewEpisodesThisWeek()
{
return Cache::remember('new_episodes_this_week', now()->addMinutes(60), function () {
return Episode::whereDate('created_at', '>=', today()->subDays(7))->count();
});
}
public static function getTopTags()
{
return Cache::remember('top_tags_stats', now()->addMinutes(120), function () {
return Tag::where('count', '>', 0)
->orderBy('count', 'desc')
->limit(10)
->get();
});
}
public static function getPopularAllTime(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';
return Cache::remember("top_hentai_alltime".$guestString, now()->addMinutes(360), function () use ($guest) {
return Cache::remember('top_hentai_alltime'.$guestString, now()->addMinutes(360), function () use ($guest) {
return Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->orderBy('view_count','desc')
->orderBy('view_count', 'desc')
->limit(16)
->get();
});
@@ -75,7 +172,7 @@ class CacheHelper
public static function getPopularMonthly()
{
return Cache::remember("top_hentai_monthly", now()->addMinutes(360), function () {
return Cache::remember('top_hentai_monthly', now()->addMinutes(360), function () {
return PopularMonthly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -87,7 +184,7 @@ class CacheHelper
public static function getPopularWeekly()
{
return Cache::remember("top_hentai_weekly", now()->addMinutes(360), function () {
return Cache::remember('top_hentai_weekly', now()->addMinutes(360), function () {
return PopularWeekly::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -100,7 +197,7 @@ class CacheHelper
public static function getPopularDaily()
{
return Cache::remember("top_hentai_daily", now()->addMinutes(30), function () {
return Cache::remember('top_hentai_daily', now()->addMinutes(30), function () {
return PopularDaily::groupBy('episode_id')
->select('episode_id', DB::raw('count(*) as total'))
->with('episode.gallery')
@@ -112,22 +209,22 @@ class CacheHelper
public static function getMostLikes()
{
return Cache::remember("top_likes", now()->addMinutes(30), function () {
return Cache::remember('top_likes', now()->addMinutes(30), function () {
return DB::table('markable_likes')->groupBy('markable_id')->select('markable_id', DB::raw('count(*) as total'))->orderBy('total', 'desc')->limit(16)->get();
});
}
public static function getAllTags()
{
return Cache::remember("all_tags", now()->addMinutes(10080), function () {
return Cache::remember('all_tags', now()->addMinutes(10080), function () {
return Tag::where('count', '>', 0)->orderBy('slug', 'ASC')->get();
});
}
public static function getLatestComments()
{
return Cache::remember("latest_comments", now()->addMinutes(60), function () {
return Comment::latest()->take(10)->get();
return Cache::remember('latest_comments', now()->addMinutes(60), function () {
return Comment::with('user')->latest()->take(10)->get();
});
}
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Conner\Tagging\Model\Tag;
class FilterCategories
{
public static function getFilterCategories()
{
$taglist = Cache::remember(
'searchtags',
300,
fn () => Tag::where('count', '>', 0)
->orderBy('slug')
->get(),
);
$appearances = [
'Loli',
'Shota',
'Milf',
'Futanari',
'Big Boobs',
'Small Boobs',
'Dark Skin',
'Cosplay',
'Elf',
'Maid',
'Nekomimi',
'Nurse',
'School Girl',
'Succubus',
'Teacher',
'Trap',
'Pregnant',
'Glasses',
'Swim Suit',
'Ugly Bastard',
'Monster',
];
$types = [
'3D',
'4K',
'48Fps',
'4K 48Fps',
'Censored',
'Uncensored',
'Comedy',
'Fantasy',
'Horror',
'Vanilla',
'Ntr',
'Pov',
'Filmed',
'X-Ray',
];
$actions = [
'Anal',
'Bdsm',
'Facial',
'Blow Job',
'Boob Job',
'Foot Job',
'Hand Job',
'Rimjob',
'Inflation',
'Masturbation',
'Public Sex',
'Rape',
'Reverse Rape',
'Threesome',
'Orgy',
'Gangbang',
];
$excluded = [...$appearances, ...$types, ...$actions];
$categories = [
'Genres' => $taglist
->reject(fn ($tag) => in_array($tag->name, $excluded))
->pluck('name')
->toArray(),
'Actions' => $actions,
'Appearance' => collect($appearances)
->reject(function ($tag) {
return Auth::guest() && in_array($tag, ['Loli', 'Shota']);
})
->toArray(),
'Types' => $types,
];
return $categories;
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ class GitHelper
{
public static function shortCommit()
{
return Cache::remember("git_commit", now()->addMinutes(60), function () {
return Cache::remember('git_commit', now()->addMinutes(60), function () {
try {
return trim(exec('git rev-parse --short HEAD'));
} catch (\Exception $e) {
@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Alert;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AlertController extends Controller
{
/**
/**
* Display alert index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.alert.index');
}
@@ -19,7 +21,7 @@ class AlertController extends Controller
/**
* Create Alert.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'message' => 'required|string|max:255',
@@ -39,7 +41,7 @@ class AlertController extends Controller
/**
* Delete Alert.
*/
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
public function delete(int $alert_id): RedirectResponse
{
Alert::where('id', $alert_id)->delete();
@@ -3,13 +3,14 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\View\View;
class CommentsController extends Controller
{
/**
* Display Comments Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.comments.index');
}
@@ -4,25 +4,27 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Contact;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$contacts = Contact::orderBy('created_at', 'DESC')->get();
return view('admin.contact.index', [
'contacts' => $contacts
'contacts' => $contacts,
]);
}
/**
* Delete Contact.
*/
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse
public function delete(int $contact_id): RedirectResponse
{
Contact::where('id', $contact_id)->delete();
@@ -2,26 +2,29 @@
namespace App\Http\Controllers\Admin;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Jobs\DiscordReleaseNotification;
use App\Models\Episode;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EpisodeController extends Controller
{
protected EpisodeService $episodeService;
protected GalleryService $galleryService;
protected DownloadService $downloadService;
public function __construct(
EpisodeService $episodeService,
GalleryService $galleryService,
EpisodeService $episodeService,
GalleryService $galleryService,
DownloadService $downloadService
) {
) {
$this->episodeService = $episodeService;
$this->galleryService = $galleryService;
$this->downloadService = $downloadService;
@@ -30,7 +33,7 @@ class EpisodeController extends Controller
/**
* Add Episode to existing series
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
@@ -43,7 +46,7 @@ class EpisodeController extends Controller
// Discord Alert
if ($request->has('censored')) {
DiscordReleaseNotification::dispatch($referenceEpisode->title." - ".$episodeNumber, 'release-censored');
DiscordReleaseNotification::dispatch($referenceEpisode->title.' - '.$episodeNumber, 'release-censored');
} else {
DiscordReleaseNotification::dispatch($episode->slug, 'release');
}
@@ -51,16 +54,27 @@ class EpisodeController extends Controller
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug
'title' => $episode->slug,
]);
}
/**
* Edit Episode
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
if ($request->user()->hasRole(UserRole::MODERATOR)) {
$this->episodeService->updateEpisodeModerator($request, $episode->id);
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug,
]);
}
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
$oldinterpolated = $episode->interpolated;
@@ -87,7 +101,7 @@ class EpisodeController extends Controller
cache()->flush();
return to_route('hentai.index', [
'title' => $episode->slug
'title' => $episode->slug,
]);
}
}
@@ -3,34 +3,37 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Hentai;
use App\Jobs\DiscordReleaseNotification;
use App\Models\Hentai;
use App\Services\DownloadService;
use App\Services\EpisodeService;
use App\Services\GalleryService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ReleaseController extends Controller
{
protected EpisodeService $episodeService;
protected GalleryService $galleryService;
protected DownloadService $downloadService;
public function __construct(
EpisodeService $episodeService,
GalleryService $galleryService,
EpisodeService $episodeService,
GalleryService $galleryService,
DownloadService $downloadService
) {
) {
$this->episodeService = $episodeService;
$this->galleryService = $galleryService;
$this->downloadService = $downloadService;
}
/**
/**
* Display release page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.release.create');
}
@@ -38,7 +41,7 @@ class ReleaseController extends Controller
/**
* Upload New Hentai with One or Multipe Episodes
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
// Create new Hentai or find existing one
$slug = $this->episodeService->generateSlug($request->input('title'));
@@ -3,22 +3,22 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\SiteBackground;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\View\View;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class SiteBackgroundController extends Controller
{
/**
/**
* Display admin index page
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.background.index', [
'images' => SiteBackground::all(),
@@ -28,15 +28,15 @@ class SiteBackgroundController extends Controller
/**
* Create new site backgrounds
*/
public function create(Request $request): \Illuminate\Http\RedirectResponse
public function create(Request $request): RedirectResponse
{
$request->validate([
'images' => 'required',
'date_start' => 'required',
'date_end' => 'required',
'images' => 'required',
'date_start' => 'required',
'date_end' => 'required',
]);
foreach($request->file('images') as $file) {
foreach ($request->file('images') as $file) {
// Initiating a database transaction in case something goes wrong.
DB::beginTransaction();
@@ -44,24 +44,25 @@ class SiteBackgroundController extends Controller
$bg = SiteBackground::create(array_merge(
$request->only(['date_start', 'date_end']),
[
'default' => (bool) $request->input('default', false)
'default' => (bool) $request->input('default', false),
]
));
$resolutions = [1440, 1080, 720, 640];
foreach($resolutions as $resolution) {
foreach ($resolutions as $resolution) {
// /images/background/1-2560p.webp
$targetPath = "/images/background/{$bg->id}-{$resolution}p.webp";
Image::read($file->getRealPath())
->scaleDown(height: $resolution)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(public_path($targetPath));
}
} catch (\Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return redirect()->back();
}
@@ -74,18 +75,18 @@ class SiteBackgroundController extends Controller
return redirect()->back();
}
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$request->validate([
'id' => 'required|exists:site_backgrounds,id',
'date_start' => 'required',
'date_end' => 'required',
'id' => 'required|exists:site_backgrounds,id',
'date_start' => 'required',
'date_end' => 'required',
]);
SiteBackground::where('id', $request->input('id'))->update(array_merge(
$request->only(['date_start', 'date_end']),
[
'default' => (bool) $request->input('default', false)
'default' => (bool) $request->input('default', false),
]
));
@@ -97,7 +98,7 @@ class SiteBackgroundController extends Controller
/**
* Delete backround
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
$id = $request->input('id');
@@ -109,16 +110,17 @@ class SiteBackgroundController extends Controller
$resolutions = [1440, 1080, 720, 640];
try {
foreach($resolutions as $resolution) {
foreach ($resolutions as $resolution) {
$targetPath = "/images/background/{$id}-{$resolution}p.webp";
File::delete(public_path($targetPath));
}
} catch (\Exception $e) {
DB::rollBack();
Log::error($e->getMessage());
return redirect()->back();
}
// Committing the database transaction.
DB::commit();
@@ -2,10 +2,11 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\EpisodeSubtitle;
use App\Models\Subtitle;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class SubtitleController extends Controller
@@ -13,11 +14,11 @@ class SubtitleController extends Controller
/**
* Add new Subtitle.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$subtitle = Subtitle::create([
'name' => $request->name,
'slug' => $request->slug,
'name' => $request->name,
'slug' => $request->slug,
]);
// Add to Episode
@@ -32,12 +33,12 @@ class SubtitleController extends Controller
/**
* Update Episode Subtitles.
*/
public function update(Request $request): \Illuminate\Http\RedirectResponse
public function update(Request $request): RedirectResponse
{
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
// Clear everything
foreach($episode->subtitles as $sub) {
foreach ($episode->subtitles as $sub) {
$sub->delete();
}
@@ -2,16 +2,18 @@
namespace App\Http\Controllers\Admin;
use App\Models\User;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UserController extends Controller
{
/**
* Display Users Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('admin.users.index');
}
@@ -22,24 +24,23 @@ class UserController extends Controller
public function update(Request $request)
{
$validated = $request->validate([
'id' => 'required|exists:users,id',
'action' => 'required',
'id' => 'required|exists:users,id',
'action' => 'required',
]);
$user = User::findOrFail($validated['id']);
switch ($validated['action']) {
case 'ban':
$user->update(['is_banned' => 1]);
$user->addRole(UserRole::BANNED);
alert()->success('Banned', 'User has been banned.');
break;
case 'unban':
$user->update(['is_banned' => 0]);
$user->removeRole(UserRole::BANNED);
alert()->success('Unbanned', 'User has been unbanned.');
break;
default:
alert()->error('Error','Invalid action provided');
alert()->error('Error', 'Invalid action provided');
}
return redirect()->back();
@@ -3,10 +3,10 @@
namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\Studios;
use App\Models\Subtitle;
use App\Http\Controllers\Controller;
class AdminApiController extends Controller
{
@@ -2,11 +2,11 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Downloads;
use App\Models\Episode;
use App\Rules\ValidCaptcha;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class DownloadApiController extends Controller
{
@@ -16,11 +16,12 @@ class DownloadApiController extends Controller
public function getDownload(Request $request)
{
$validated = $request->validate([
'episode_id' => 'required',
'captcha' => 'required|captcha'
'episode_id' => ['required'],
'captcha' => ['required', new ValidCaptcha],
]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
$episode = Episode::where('id', $request->input('episode_id'))
->firstOrFail();
// Increase download count, as we assume the user
// downloads after submitting the captcha
@@ -30,9 +31,9 @@ class DownloadApiController extends Controller
$download->save();
return response()->json([
'message' => 'success',
'download_url' => $download->url,
'download_count' => $oldCount,
'message' => 'success',
'download_url' => $download->url,
'download_count' => $oldCount,
], 200);
}
}
@@ -2,12 +2,11 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Hentai;
use App\Models\PopularMonthly;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class HentaiApiController extends Controller
{
@@ -23,13 +22,13 @@ class HentaiApiController extends Controller
->get()
->map(function ($hentai) {
return [
'title' => $hentai->episodes[0]->title,
'title' => $hentai->episodes[0]->title,
'title_jpn' => $hentai->episodes[0]->title_jpn,
'slug' => $hentai->slug,
'episodes' => $hentai->episodes->map(function ($ep) {
'slug' => $hentai->slug,
'episodes' => $hentai->episodes->map(function ($ep) {
return [
'episode' => $ep->episode,
'slug' => $ep->slug,
'episode' => $ep->episode,
'slug' => $ep->slug,
];
}),
];
+116 -12
View File
@@ -3,10 +3,15 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Episode;
use App\Models\VideoEngagement;
use App\Models\Watched;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
class StreamApiController extends Controller
{
@@ -16,24 +21,123 @@ class StreamApiController extends Controller
public function getStream(Request $request)
{
$validated = $request->validate([
'episode_id' => 'required',
'episode_id' => 'required',
]);
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
$subtitles = $episode->subtitles
->mapWithKeys(fn($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
->mapWithKeys(fn ($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
->toArray();
return response()->json([
'title' => $episode->title.' - '.$episode->episode,
'poster' => $episode->gallery()->first()->image_url,
'interpolated' => $episode->interpolated,
'interpolated_uhd' => $episode->interpolated_uhd,
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
'stream_domains' => config('hstream.stream_domain'),
'asia_stream_domains' => config('hstream.asia_stream_domain'),
'extra_subtitles' => $subtitles
'title' => $episode->title.' - '.$episode->episode,
'poster' => $episode->gallery()->first()->image_url,
'interpolated' => $episode->interpolated,
'interpolated_uhd' => $episode->interpolated_uhd,
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
'stream_domains' => config('hstream.stream_domain'),
'asia_stream_domains' => config('hstream.asia_stream_domain'),
'extra_subtitles' => $subtitles,
], 200);
}
/**
* Track that the authenticated user has watched the episode.
* Called client-side after 10 seconds of playback.
*/
public function trackWatched(Request $request): JsonResponse
{
$validated = $request->validate([
'episode_id' => 'required|integer|exists:episodes,id',
]);
$user = Auth::user();
if (! $user) {
return response()->json(['watched' => false], 401);
}
$episodeId = $request->input('episode_id');
// 1-hour cooldown to prevent duplicate entries
$time = Carbon::now()->subHour(1);
$alreadyWatched = Watched::where('user_id', $user->id)
->where('episode_id', $episodeId)
->where('created_at', '>=', $time)
->exists();
if (! $alreadyWatched) {
Watched::create(['user_id' => $user->id, 'episode_id' => $episodeId]);
cache()->forget('user'.$user->id.'watched'.$episodeId);
}
return response()->json(['watched' => true], 200);
}
/**
* Track engagement segments that the authenticated user has watched.
* Called periodically client-side while the video is playing.
* Segments are 10-second chunks; segment 0 (0-10s) is excluded.
*/
public function trackEngagement(Request $request): JsonResponse
{
$validated = $request->validate([
'episode_id' => 'required|integer|exists:episodes,id',
'segments' => 'required|array',
'segments.*' => 'integer|min:1',
]);
$user = Auth::user();
if (! $user) {
return response()->json(['tracked' => 0], 401);
}
$rateLimitKey = 'engagement:'.$user->id;
if (RateLimiter::tooManyAttempts($rateLimitKey, 10)) {
$seconds = RateLimiter::availableIn($rateLimitKey);
return response()->json([
'tracked' => 0,
'message' => 'Rate limit exceeded. Try again in '.$seconds.' seconds.',
], 429);
}
RateLimiter::hit($rateLimitKey, 60);
$episodeId = $request->input('episode_id');
$segments = $request->input('segments');
$tracked = 0;
foreach ($segments as $segment) {
// upsert: insert if not exists, otherwise ignore (unique constraint prevents dupes)
VideoEngagement::firstOrCreate([
'episode_id' => $episodeId,
'user_id' => $user->id,
'segment' => $segment,
]);
$tracked++;
}
return response()->json(['tracked' => $tracked], 200);
}
/**
* Get engagement heatmap data for an episode.
* Returns watch counts per segment, aggregated across all users.
*/
public function getEngagement(int $episodeId): JsonResponse
{
$episode = Episode::findOrFail($episodeId);
$data = cache()->remember(
"engagement:{$episodeId}",
600, // 10-minute cache
fn () => VideoEngagement::where('episode_id', $episodeId)
->select('segment', DB::raw('count(*) as watch_count'))
->groupBy('segment')
->pluck('watch_count', 'segment')
->toArray()
);
return response()->json($data, 200);
}
}
@@ -4,9 +4,8 @@ namespace App\Http\Controllers\Api;
use App\Helpers\CacheHelper;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Conner\Tagging\Model\Tag;
use Illuminate\Http\Request;
class UserApiController extends Controller
{
@@ -33,11 +32,10 @@ class UserApiController extends Controller
}
}
return response()->json([
'message' => 'success',
'tags' => $tagWhiteList,
'usertags' => $tagBlackList
'message' => 'success',
'tags' => $tagWhiteList,
'usertags' => $tagBlackList,
], 200);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\CreateChallengeOptions;
use App\Http\Controllers\Controller;
class CaptchaController extends Controller
{
public function create(): array
{
$pbkdf2 = new Pbkdf2;
$altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
// Create challenge
$challenge = $altcha->createChallenge(new CreateChallengeOptions(
algorithm: $pbkdf2,
cost: 5000,
counter: random_int(5000, 10000),
expiresAt: time() + 600,
));
return get_object_vars($challenge);
}
}
@@ -2,15 +2,15 @@
namespace App\Http\Controllers\Auth;
use App\Models\User;
use App\Enums\UserRole;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\Facades\Socialite;
use Laravel\Socialite\Two\InvalidStateException;
class DiscordAuthController extends Controller
{
@@ -27,11 +27,16 @@ class DiscordAuthController extends Controller
*/
public function callback(): RedirectResponse
{
$discordUser = Socialite::driver('discord')->user();
try {
$discordUser = Socialite::driver('discord')->user();
} catch (InvalidStateException $e) {
return redirect()->route('login')
->with('error', 'Your login session expired. Please try signing in again.');
}
$user = User::where('discord_id', $discordUser->id)->first();
if (!$user) {
if (! $user) {
// link by email if it already exists
$user = User::where('email', $discordUser->email)->first();
@@ -76,7 +81,7 @@ class DiscordAuthController extends Controller
private function checkDiscordRoles(User $user): void
{
// Should not ever happen
if (!$user->discord_id) {
if (! $user->discord_id) {
return;
}
@@ -88,9 +93,7 @@ class DiscordAuthController extends Controller
// User is not in the guild
if ($response->status() === 404) {
$user->update([
'is_patreon' => false,
]);
$user->removeRole(UserRole::SUPPORTER);
return;
}
@@ -110,21 +113,16 @@ class DiscordAuthController extends Controller
$discordRoles = $response->json('roles', []);
$patreonRoles = config('discord.patreon_roles', []);
$isPatreon = false;
foreach($patreonRoles as $patreonRole)
{
if (in_array($patreonRole, $discordRoles, true)) {
$isPatreon = true;
break;
}
// If intersect of array is empty, then the user doesn't have the role
$hasSupporterRole = ! empty(array_intersect($discordRoles, $patreonRoles));
if (! $hasSupporterRole) {
// Remove role if not found
$user->removeRole(UserRole::SUPPORTER);
return;
}
// Only update if something actually changed
if ($user->is_patreon !== $isPatreon) {
$user->update([
'is_patreon' => $isPatreon,
]);
}
$user->addRole(UserRole::SUPPORTER);
}
}
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class NewPasswordController extends Controller
@@ -26,7 +27,7 @@ class NewPasswordController extends Controller
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -16,8 +16,7 @@ class PasswordController extends Controller
public function update(Request $request): RedirectResponse
{
// If user logged in with Discord and has not yet a password, allow to set password
if ($request->user()->discord_id && is_null($request->user()->password))
{
if ($request->user()->discord_id && is_null($request->user()->password)) {
$validated = $request->validateWithBag('updatePassword', [
'password' => ['required', Password::defaults(), 'confirmed'],
]);
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -4,19 +4,21 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Validation\ValidationException;
class RegisteredUserController extends Controller
{
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request): RedirectResponse
{
@@ -24,6 +26,7 @@ class RegisteredUserController extends Controller
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'altcha' => ['required', new ValidCaptcha],
]);
$user = User::create([
+9 -11
View File
@@ -3,14 +3,17 @@
namespace App\Http\Controllers;
use App\Models\Contact;
use App\Rules\ValidCaptcha;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class ContactController extends Controller
{
/**
* Display Contact Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('contact.form');
}
@@ -18,17 +21,17 @@ class ContactController extends Controller
/**
* Store Contact Submission.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'email' => 'required|max:50',
'name' => 'required|max:30',
'email' => 'required|max:50',
'message' => 'required|max:1000',
'subject' => 'required|max:50',
'captcha' => 'required|captcha',
'altcha' => ['required', new ValidCaptcha],
]);
$contact = new Contact();
$contact = new Contact;
$contact->name = $request->input('name');
$contact->email = $request->input('email');
$contact->message = $request->input('message');
@@ -37,9 +40,4 @@ class ContactController extends Controller
return back()->with('status', 'contact-submitted');
}
public function reloadCaptcha(): \Illuminate\Http\JsonResponse
{
return response()->json(['captcha'=> captcha_img()]);
}
}
+50 -36
View File
@@ -2,52 +2,52 @@
namespace App\Http\Controllers;
use App\Models\Episode;
use App\Helpers\CacheHelper;
use App\Models\Episode;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cookie;
use Illuminate\View\View;
class HomeController extends Controller
{
/**
* Display Home Page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
$guest = Auth::guest();
$guestString = $guest ? 'guest' : 'authed';
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () =>
Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->whereIn('id', function($query) {
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
$query->selectRaw('id')
->from('episodes')
->whereIn('id', $mostLikesIds)
->orderByRaw("FIELD(id, " . implode(',', $mostLikesIds) . ")");
})
->get()
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () => Episode::with('gallery')
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
->whereIn('id', function ($query) {
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
$query->selectRaw('id')
->from('episodes')
->whereIn('id', $mostLikesIds)
->orderByRaw('FIELD(id, '.implode(',', $mostLikesIds).')');
})
->get()
);
return view('home.index', [
'recentlyReleased' => CacheHelper::getRecentlyReleased($guest),
'recentlyUploaded' => CacheHelper::getRecentlyUploaded($guest),
'popularAllTime' => CacheHelper::getPopularAllTime($guest),
'popularMonthly' => CacheHelper::getPopularMonthly(),
'popularWeekly' => CacheHelper::getPopularWeekly(),
'popularDaily' => CacheHelper::getPopularDaily(),
'mostLikes' => $mostLikes,
'latestComments' => CacheHelper::getLatestComments(),
'recentlyReleased' => CacheHelper::getRecentlyReleased($guest),
'recentlyUploaded' => CacheHelper::getRecentlyUploaded($guest),
'popularAllTime' => CacheHelper::getPopularAllTime($guest),
'popularMonthly' => CacheHelper::getPopularMonthly(),
'popularWeekly' => CacheHelper::getPopularWeekly(),
'popularDaily' => CacheHelper::getPopularDaily(),
'mostLikes' => $mostLikes,
'latestComments' => CacheHelper::getLatestComments(),
]);
}
/**
* Display Banned Page.
*/
public function banned(): \Illuminate\View\View
public function banned(): View
{
return view('auth.banned');
}
@@ -56,7 +56,7 @@ class HomeController extends Controller
* Redirects to a random Hentai episode
* Done due to performance reasons
*/
public function random(): \Illuminate\Http\RedirectResponse
public function random(): RedirectResponse
{
$random = Episode::inRandomOrder()
->limit(1)
@@ -71,7 +71,7 @@ class HomeController extends Controller
/**
* Display Search Page.
*/
public function search(): \Illuminate\View\View
public function search(): View
{
return view('search.index');
}
@@ -79,7 +79,7 @@ class HomeController extends Controller
/**
* Display Download Search Page.
*/
public function downloadSearch(): \Illuminate\View\View
public function downloadSearch(): View
{
return view('search.download');
}
@@ -87,7 +87,7 @@ class HomeController extends Controller
/**
* Redirect POST Data to GET with Query String.
*/
public function searchRedirect(Request $request): \Illuminate\Http\RedirectResponse
public function searchRedirect(Request $request): RedirectResponse
{
return redirect()->route('hentai.search', [
'search' => $request->input('live-search'),
@@ -97,25 +97,39 @@ class HomeController extends Controller
/**
* Display Stats Page.
*/
public function stats(): \Illuminate\View\View
public function stats(): View
{
return view('home.stats', [
'viewCount' => CacheHelper::getTotalViewCount(),
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
'viewCount' => CacheHelper::getTotalViewCount(),
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
'userCount' => CacheHelper::getTotalUserCount(),
'commentCount' => CacheHelper::getTotalCommentCount(),
'likeCount' => CacheHelper::getTotalLikeCount(),
'downloadCount' => CacheHelper::getTotalDownloadCount(),
'episodes4k' => CacheHelper::get4kEpisodeCount(),
'episodesUHD48' => CacheHelper::getUHD48FpsEpisodeCount(),
'todayViews' => CacheHelper::getTodayViewCount(),
'weeklyViews' => CacheHelper::getWeeklyViewCount(),
'prevWeeklyViews' => CacheHelper::getPreviousWeeklyViewCount(),
'avgViewsPerEpisode' => CacheHelper::getAverageViewsPerEpisode(),
'newEpisodesThisWeek' => CacheHelper::getNewEpisodesThisWeek(),
'topTags' => CacheHelper::getTopTags(),
]);
}
/**
* Manually set website language
*/
public function updateLanguage(Request $request): \Illuminate\Http\RedirectResponse
public function updateLanguage(Request $request): RedirectResponse
{
if(! in_array($request->language, config('lang-detector.languages'))) {
return redirect()->back();
}
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
Cookie::queue(Cookie::forever('locale', $request->language));
session(['locale' => $request->language]);
if (Auth::check()) {
Auth::user()->update(['locale' => $request->language]);
}
return redirect()->back();
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\MatrixRegisterRequest;
use App\Services\MatrixRegistrationService;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MatrixController extends Controller
{
/**
* Display the user page.
*/
public function index(Request $request): View
{
$rooms = [
['name' => '🏠 General', 'description' => 'Our main chat.', 'alias' => 'https://matrix.to/#/#general:hstream.moe'],
['name' => '📡 Releases', 'description' => 'Were we @everyone for new releases.', 'alias' => 'https://matrix.to/#/#releases:hstream.moe'],
['name' => '👗 NSFW 2D', 'description' => 'Channel for R18 2D Media.', 'alias' => 'https://matrix.to/#/#nsfw:hstream.moe'],
['name' => '👗 NSFW IRL', 'description' => 'Channel for R18 IRL Media.', 'alias' => 'https://matrix.to/#/#nsfw-irl:hstream.moe'],
];
return view('matrix.index', [
'user' => $request->user(),
'rooms' => $rooms,
]);
}
/**
* Create matrix user
*/
public function store(
MatrixRegisterRequest $request,
MatrixRegistrationService $matrixService
) {
try {
$result = $matrixService->registerUser(
$request->username,
$request->password
);
$user = $request->user();
$user->matrix_id = $result['user_id'];
$user->save();
return redirect()
->back()
->with('success', 'Matrix user created successfully.');
} catch (\Exception $e) {
return back()
->withErrors([
'username' => $e->getMessage(),
])
->withInput();
}
}
}
@@ -2,29 +2,37 @@
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class NotificationController extends Controller
{
/**
* Display the user's notification page.
*/
public function index(Request $request): \Illuminate\View\View
public function index(Request $request): View
{
return view('profile.notifications', [
'user' => $request->user(),
'user' => $request->user(),
'notifications' => $request->user()->unreadNotifications,
]);
}
/**
* Delete Notifcation
* Delete a notification or clear all.
*/
public function delete(Request $request): \Illuminate\Http\RedirectResponse
public function delete(Request $request): RedirectResponse
{
// Clear all notifications if no specific ID is provided
if (! $request->has('id')) {
$request->user()->notifications()->delete();
return redirect()->back()->with('status', 'notifications-cleared');
}
$request->validate([
'id' => 'required|exists:notifications,id',
'id' => 'required|exists:notifications,id',
]);
$notification = $request->user()
+59 -30
View File
@@ -6,8 +6,10 @@ use App\Models\Episode;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Services\PlaylistService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use RealRashid\SweetAlert\Facades\Alert;
use Illuminate\View\View;
class PlaylistController extends Controller
{
@@ -21,7 +23,7 @@ class PlaylistController extends Controller
/**
* Display the public playlists page.
*/
public function index(): \Illuminate\View\View
public function index(): View
{
return view('playlist.index');
}
@@ -29,9 +31,9 @@ class PlaylistController extends Controller
/**
* Display public playlist.
*/
public function show($playlist_id): \Illuminate\View\View
public function show($playlist_id): View
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -42,18 +44,17 @@ class PlaylistController extends Controller
]);
}
/**
* Display the user's playlists page.
*/
public function playlists(Request $request): \Illuminate\View\View
public function playlists(Request $request): View
{
$title = 'Delete Playlist!';
$text = "Are you sure you want to delete?";
$text = 'Are you sure you want to delete?';
confirmDelete($title, $text);
return view('profile.playlists', [
'user' => $request->user(),
'user' => $request->user(),
'playlists' => $request->user()->playlists,
]);
}
@@ -61,9 +62,9 @@ class PlaylistController extends Controller
/**
* Display user's playlist.
*/
public function showPlaylist(Request $request, $playlist_id): \Illuminate\View\View
public function showPlaylist(Request $request, $playlist_id): View
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -79,13 +80,13 @@ class PlaylistController extends Controller
/**
* Create user playlist (Form).
*/
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse
public function createPlaylist(Request $request): RedirectResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'name' => 'required|max:30',
]);
$playlist = new Playlist();
$playlist = new Playlist;
$playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private';
@@ -94,12 +95,40 @@ class PlaylistController extends Controller
return to_route('profile.playlists');
}
/**
* Update user playlist.
*/
public function updatePlaylist(Request $request, $playlist_id): RedirectResponse
{
if (! is_numeric($playlist_id)) {
abort(404);
}
$validated = $request->validate([
'name' => 'required|max:30',
'is_private' => 'required|boolean',
]);
$user = $request->user();
$playlist = Playlist::where('user_id', $user->id)
->where('id', $playlist_id)
->firstOrFail();
$playlist->update([
'name' => $request->input('name'),
'is_private' => $request->input('is_private'),
]);
return back()->with('status', 'playlist-updated');
}
/**
* Delete user playlist.
*/
public function deletePlaylist(Request $request, $playlist_id): \Illuminate\Http\RedirectResponse
public function deletePlaylist(Request $request, $playlist_id): RedirectResponse
{
if (!is_numeric($playlist_id)) {
if (! is_numeric($playlist_id)) {
abort(404);
}
@@ -117,12 +146,12 @@ class PlaylistController extends Controller
/**
* Delete episode from playlist.
*/
public function deleteEpisodeFromPlaylist(Request $request): \Illuminate\Http\JsonResponse
public function deleteEpisodeFromPlaylist(Request $request): JsonResponse
{
if (!is_numeric($request->input('playlist')) || !is_numeric($request->input('episode'))) {
if (! is_numeric($request->input('playlist')) || ! is_numeric($request->input('episode'))) {
return response()->json([
'message' => 'not-numeric',
'user' => $request->user(),
'user' => $request->user(),
], 404);
}
@@ -133,25 +162,25 @@ class PlaylistController extends Controller
PlaylistEpisode::where('playlist_id', $playlist->id)
->where('episode_id', (int) $request->input('episode'))
->delete();
$this->playlistService->reorderPositions($playlist);
return response()->json([
'message' => 'success',
'user' => $request->user(),
'user' => $request->user(),
], 200);
}
/**
* Add to user playlist (API).
*/
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function addPlaylistApi(Request $request): JsonResponse
{
$user = $request->user();
$validated = $request->validate([
'playlist' => 'required|max:30',
'episode_id' => 'required'
'playlist' => 'required|max:30',
'episode_id' => 'required',
]);
$playlist = Playlist::where('user_id', $user->id)->where('id', $request->input('playlist'))->firstOrFail();
@@ -161,7 +190,7 @@ class PlaylistController extends Controller
$exists = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->exists();
if ($exists) {
return response()->json([
'message' => 'already-added'
'message' => 'already-added',
], 200);
}
@@ -175,28 +204,28 @@ class PlaylistController extends Controller
]);
return response()->json([
'message' => 'success'
'message' => 'success',
], 200);
}
/**
* Create user playlist (API).
*/
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
public function createPlaylistApi(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|max:30',
'name' => 'required|max:30',
]);
$playlist = new Playlist();
$playlist = new Playlist;
$playlist->user_id = $request->user()->id;
$playlist->name = $request->input('name');
$playlist->is_private = $request->input('visiblity') === 'private';
$playlist->save();
return response()->json([
'message' => 'success',
'playlist_id' => $playlist->id
'message' => 'success',
'playlist_id' => $playlist->id,
], 200);
}
}
+14 -15
View File
@@ -2,22 +2,21 @@
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use App\Models\Episode;
use App\Models\User;
use App\Http\Requests\ProfileUpdateRequest;
use Conner\Tagging\Model\Tag;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\View\View;
use Intervention\Image\Laravel\Facades\Image;
use Conner\Tagging\Model\Tag;
class ProfileController extends Controller
{
/**
@@ -38,8 +37,8 @@ class ProfileController extends Controller
$example = Episode::where('title', 'Succubus Yondara Gibo ga Kita!?')->first();
return view('profile.settings', [
'user' => $request->user(),
'example' => $example,
'user' => $request->user(),
'example' => $example,
]);
}
@@ -92,7 +91,7 @@ class ProfileController extends Controller
public function likes(Request $request): View
{
return view('profile.likes', [
'user' => $request->user(),
'user' => $request->user(),
]);
}
@@ -118,9 +117,10 @@ class ProfileController extends Controller
$user = $request->user();
$tags = json_decode($request->input('tags'));
if (!$tags) {
if (! $tags) {
$user->tag_blacklist = null;
$user->save();
return Redirect::route('profile.settings')->with('status', 'blacklist-updated');
}
@@ -139,12 +139,12 @@ class ProfileController extends Controller
/**
* Delete the user's account.
*/
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
public function destroy(Request $request): RedirectResponse
{
$user = $request->user();
// Verify password if user has password
if (!is_null($user->password)) {
if (! is_null($user->password)) {
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
@@ -174,11 +174,11 @@ class ProfileController extends Controller
/**
* Store custom user avatar.
*/
protected function storeAvatar(\Illuminate\Http\UploadedFile $file, User $user): void
protected function storeAvatar(UploadedFile $file, User $user): void
{
// Create Folder for Image Upload
if (! Storage::disk('public')->exists("/images/avatars")) {
Storage::disk('public')->makeDirectory("/images/avatars");
if (! Storage::disk('public')->exists('/images/avatars')) {
Storage::disk('public')->makeDirectory('/images/avatars');
}
// Delete old avatar if it exists
@@ -196,5 +196,4 @@ class ProfileController extends Controller
$user->avatar = $filename;
}
}
+21 -37
View File
@@ -2,26 +2,23 @@
namespace App\Http\Controllers;
use App\Helpers\CacheHelper;
use App\Models\Episode;
use App\Models\Gallery;
use App\Models\Hentai;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Models\Watched;
use App\Helpers\CacheHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use hisorange\BrowserDetect\Facade as Browser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class StreamController extends Controller
{
/**
* Display Stream Page.
*/
public function index(Request $request, string $title): \Illuminate\View\View
public function index(Request $request, string $title): View
{
$titleParts = explode('-', $title);
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
@@ -32,12 +29,11 @@ class StreamController extends Controller
}
return view('series.index', [
'hentai' => $hentai,
'hentai' => $hentai,
'popularWeekly' => CacheHelper::getPopularWeekly(),
]);
}
$episode = Episode::where('slug', $title)->firstOrFail();
$gallery = Gallery::where('episode_id', $episode->id)->get();
$moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get();
@@ -54,18 +50,6 @@ class StreamController extends Controller
// Increment Popular Count
$episode->incrementPopularCount();
if (!Auth::guest()) {
$user = Auth::user();
// Add to user watched list
$time = Carbon::now()->subHour(1);
$alreadyWatched = Watched::where('user_id', $user->id)->where('episode_id', $episode->id)->where('created_at', '>=', $time)->exists();
if (!$alreadyWatched) {
Watched::create(['user_id' => $user->id, 'episode_id' => $episode->id]);
cache()->forget('user' . $user->id . 'watched' . $episode->id);
}
}
// Mobile Detection
$isMobile = Browser::isMobile();
@@ -81,29 +65,29 @@ class StreamController extends Controller
$playlistEpisodes = $playlist->episodes()->orderBy('position')->get();
// Check if authorized
if ($playlist->is_private && (Auth::guest() || (!Auth::guest() && Auth::user()->id != $playlist->user_id))) {
if ($playlist->is_private && (Auth::guest() || (! Auth::guest() && Auth::user()->id != $playlist->user_id))) {
abort(404);
}
return view('stream.index', [
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'playlist' => $playlist,
'playlistEpisodes' => $playlistEpisodes,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'playlist' => $playlist,
'playlistEpisodes' => $playlistEpisodes,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
]);
}
return view('stream.index', [
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
'episode' => $episode,
'moreEpisodes' => $moreEpisodes,
'studioEpisodes' => $studioEpisodes,
'gallery' => $gallery,
'popularWeekly' => CacheHelper::getPopularWeekly(),
'isMobile' => $isMobile,
]);
}
}
+56 -27
View File
@@ -2,7 +2,34 @@
namespace App\Http;
use App\Http\Middleware\Authenticate;
use App\Http\Middleware\EncryptCookies;
use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsBanned;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\PreventRequestsDuringMaintenance;
use App\Http\Middleware\RedirectIfAuthenticated;
use App\Http\Middleware\SetLocale;
use App\Http\Middleware\TrimStrings;
use App\Http\Middleware\TrustProxies;
use App\Http\Middleware\ValidateSignature;
use App\Http\Middleware\VerifyCsrfToken;
use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth;
use Illuminate\Auth\Middleware\Authorize;
use Illuminate\Auth\Middleware\EnsureEmailIsVerified;
use Illuminate\Auth\Middleware\RequirePassword;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
use Illuminate\Http\Middleware\HandleCors;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class Kernel extends HttpKernel
{
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
TrustProxies::class,
HandleCors::class,
PreventRequestsDuringMaintenance::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
];
/**
@@ -30,19 +57,20 @@ class Kernel extends HttpKernel
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\IsBanned::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
IsBanned::class,
SetLocale::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
ThrottleRequests::class.':api',
SubstituteBindings::class,
],
];
@@ -54,17 +82,18 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'auth' => Authenticate::class,
'auth.basic' => AuthenticateWithBasicAuth::class,
'auth.session' => AuthenticateSession::class,
'auth.admin' => IsAdmin::class,
'auth.moderator' => IsModerator::class,
'cache.headers' => SetCacheHeaders::class,
'can' => Authorize::class,
'guest' => RedirectIfAuthenticated::class,
'password.confirm' => RequirePassword::class,
'precognitive' => HandlePrecognitiveRequests::class,
'signed' => ValidateSignature::class,
'throttle' => ThrottleRequests::class,
'verified' => EnsureEmailIsVerified::class,
];
}
+15 -31
View File
@@ -1,44 +1,28 @@
<?php namespace app\Http\Middleware;
<?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Contracts\Auth\Guard;
class IsAdmin {
/**
* The Guard implementation.
*
* @var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* @param Guard $auth
* @return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsAdmin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next): Response
{
if( ! $this->auth->user()->is_admin)
{
session()->flash('error_msg','This resource is restricted to Administrators!');
return redirect()->route('home.index');
if (Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR)) {
return $next($request);
}
return $next($request);
}
session()->flash('error_msg', 'This resource is restricted to Administrators!');
return redirect()->route('home.index');
}
}
+11 -10
View File
@@ -1,29 +1,30 @@
<?php namespace app\Http\Middleware;
<?php
namespace app\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Contracts\Auth\Guard;
class IsBanned {
use Symfony\Component\HttpFoundation\Response;
class IsBanned
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
public function handle(Request $request, Closure $next): Response
{
if(auth()->check() && auth()->user()->is_banned == 1)
{
if (Auth::check() && Auth::user()->hasRole(UserRole::BANNED)) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('home.banned');
}
return $next($request);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRole;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class IsModerator
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check() && (
Auth::user()->hasRole(UserRole::MODERATOR) ||
Auth::user()->hasRole(UserRole::ADMINISTRATOR))) {
return $next($request);
}
session()->flash('error_msg', 'This resource is restricted to Moderators!');
return redirect()->route('home.index');
}
}
@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
/**
* Handle an incoming request.
*
* @param Closure(Request): (Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// 1. Logged-in user preference
if (Auth::check() && Auth::user()->locale) {
App::setLocale(Auth::user()->locale);
return $next($request);
}
// 2. Session (guest or user override)
if ($request->session()->has('locale') &&
in_array(session('locale'), config('app.supported_locales'), true)) {
App::setLocale(session('locale'));
return $next($request);
}
// 3. Browser language
$locale = $request->getPreferredLanguage(config('app.supported_locales'));
if ($locale) {
App::setLocale($locale);
}
return $next($request);
}
}
+6 -3
View File
@@ -2,7 +2,9 @@
namespace App\Http\Requests\Auth;
use App\Rules\ValidCaptcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
@@ -22,20 +24,21 @@ class LoginRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'altcha' => ['required', new ValidCaptcha],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function authenticate(): void
{
@@ -55,7 +58,7 @@ class LoginRequest extends FormRequest
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function ensureIsNotRateLimited(): void
{
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Requests;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
class MatrixRegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
$isOldEnough = $this->user()->created_at->lt(now()->subMonth());
$noAccount = ! $this->user()->matrix_id;
return $isOldEnough && $noAccount;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'username' => [
'required',
'string',
'min:3',
'max:32',
'regex:/^[a-z0-9._=-]+$/', // Valid Matrix localpart
],
'password' => [
'required',
'string',
'min:8',
'confirmed',
],
];
}
public function messages(): array
{
return [
'username.regex' => 'Username may only contain lowercase letters, numbers and . _ = -',
];
}
}
+3 -2
View File
@@ -3,6 +3,7 @@
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
@@ -21,7 +22,7 @@ class ProfileUpdateRequest extends FormRequest
'nullable',
'image',
'mimes:jpg,png,jpeg,webp,gif',
'max:8192'
'max:8192',
],
'email' => [
'required',
+10 -12
View File
@@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Spatie\DiscordAlerts\Facades\DiscordAlert;
class DiscordReleaseNotification implements ShouldQueue
@@ -32,26 +31,25 @@ class DiscordReleaseNotification implements ShouldQueue
*/
public function handle(): void
{
switch($this->messageType)
{
switch ($this->messageType) {
case 'release':
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'release-censored':
# Because Discord TOS
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release: ".$this->slug." - *No link here because of* :pLoli:");
// Because Discord TOS
DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release: '.$this->slug.' - *No link here because of* :pLoli:');
break;
case 'update':
# 1080p 48fps added
DiscordAlert::to('update')->message("<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// 1080p 48fps added
DiscordAlert::to('update')->message('<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'updateUHD':
# 4k 48fps added
DiscordAlert::to('update')->message("<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// 4k 48fps added
DiscordAlert::to('update')->message('<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
case 'v2':
# v2 re-release
DiscordAlert::to('rerelease')->message("<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
// v2 re-release
DiscordAlert::to('rerelease')->message('<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
break;
default:
break;
+7 -6
View File
@@ -7,12 +7,12 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Client\RequestException;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GetFileSizeFromCDN implements ShouldQueue
{
@@ -35,8 +35,9 @@ class GetFileSizeFromCDN implements ShouldQueue
{
// Retrieve the download record, return if not found
$download = Downloads::find($this->downloadId);
if (!$download) {
if (! $download) {
Log::error("Download not found for ID: {$this->downloadId}");
return;
}
@@ -52,7 +53,7 @@ class GetFileSizeFromCDN implements ShouldQueue
try {
// Send HTTP request to the endpoint
$response = Http::get($endpoint . '/getSize/' . $file . '/' . $expire);
$response = Http::get($endpoint.'/getSize/'.$file.'/'.$expire);
// Check if response is successful
if ($response->successful()) {
@@ -67,9 +68,9 @@ class GetFileSizeFromCDN implements ShouldQueue
Log::error("Failed to retrieve size for download ID: {$this->downloadId}, HTTP status: {$response->status()}");
}
} catch (RequestException $e) {
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: " . $e->getMessage());
Log::error("HTTP request failed for download ID: {$this->downloadId}, error: ".$e->getMessage());
} catch (\Exception $e) {
Log::error("An error occurred for download ID: {$this->downloadId}, error: " . $e->getMessage());
Log::error("An error occurred for download ID: {$this->downloadId}, error: ".$e->getMessage());
}
}
}
+126 -12
View File
@@ -2,44 +2,158 @@
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\DB;
class AdminCommentSearch extends Component
{
use WithPagination;
#[Url(history: true)]
public $search = '';
#[Url(history: true)]
public $userSearch = '';
public function updatingSearch(): void
#[Url(history: true)]
public $sortField = 'created_at';
#[Url(history: true)]
public $sortDirection = 'desc';
#[Url(history: true)]
public $perPage = 20;
public $selected = [];
public $selectPage = false;
protected $queryString = [
'search' => ['except' => ''],
'userSearch' => ['except' => ''],
'sortField' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
'perPage' => ['except' => 20],
];
protected $allowedSortFields = ['id', 'body', 'created_at', 'user_id'];
protected $allowedPerPages = [10, 20, 50, 100];
public function updatedPerPage(): void
{
$this->resetPage();
}
public function updatingUserSearch(): void
public function updatedPage(): void
{
$this->selectPage = false;
$this->selected = [];
}
public function updatedSelectPage($value): void
{
if ($value) {
$this->selected = $this->comments->pluck('id')->map(fn ($id) => (string) $id)->toArray();
} else {
$this->selected = [];
}
}
public function sortBy(string $field): void
{
if (! in_array($field, $this->allowedSortFields)) {
return;
}
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function clearFilters(): void
{
$this->search = '';
$this->userSearch = '';
$this->sortField = 'created_at';
$this->sortDirection = 'desc';
$this->perPage = 20;
$this->resetPage();
}
public function deleteComment($commentId)
public function deleteComment(int $commentId): void
{
$comment = Comment::where('id', (int) $commentId)->firstOrFail();
$comment = Comment::findOrFail($commentId);
$comment->delete();
cache()->flush();
$this->dispatch('notify', type: 'success', message: 'Comment deleted successfully.');
}
public function bulkDelete(): void
{
$count = Comment::whereIn('id', array_map('intval', $this->selected))->delete();
cache()->flush();
$this->selected = [];
$this->selectPage = false;
$this->dispatch('notify', type: 'success', message: "{$count} comment(s) deleted.");
}
public function banCommentAuthor(int $commentId): void
{
$comment = Comment::findOrFail($commentId);
$user = $comment->user;
if ($user && ! $user->hasRole(UserRole::BANNED)) {
$user->addRole(UserRole::BANNED);
cache()->flush();
$this->dispatch('notify', type: 'success', message: "{$user->name} has been banned.");
} else {
$this->dispatch('notify', type: 'error', message: 'User is already banned or not found.');
}
}
public function bulkBanAuthors(): void
{
$count = 0;
$userIds = Comment::whereIn('id', array_map('intval', $this->selected))
->pluck('user_id')
->unique();
foreach ($userIds as $userId) {
$user = User::find($userId);
if ($user && ! $user->hasRole(UserRole::BANNED)) {
$user->addRole(UserRole::BANNED);
$count++;
}
}
cache()->flush();
$this->selected = [];
$this->selectPage = false;
$this->dispatch('notify', type: 'success', message: "{$count} comment author(s) banned.");
}
public function getCommentsProperty()
{
return Comment::query()
->with('user')
->when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%{$this->search}%"))
->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($q) => $q->where('name', 'LIKE', "%{$this->userSearch}%")))
->orderBy($this->sortField, $this->sortDirection)
->paginate((int) $this->perPage);
}
public function render()
{
$comments = Comment::when($this->search !== '', fn ($query) => $query->where('body', 'LIKE', "%$this->search%"))
->when($this->userSearch !== '', fn ($query) => $query->whereHas('user', fn ($query) => $query->where('name', 'LIKE', "%{$this->userSearch}%")))
->orderBy('created_at', 'DESC')
->paginate(12);
return view('livewire.admin-comment-search', [
'comments' => $comments
'comments' => $this->comments,
]);
}
}
}
+226 -21
View File
@@ -2,13 +2,12 @@
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\DB;
class AdminUserSearch extends Component
{
@@ -18,36 +17,242 @@ class AdminUserSearch extends Component
public $search = '';
#[Url(history: true)]
public $filtered = ['true'];
public $discordId = '';
#[Url(history: true)]
public $patreon = [];
public $email = '';
#[Url(history: true)]
public $banned = [];
public $roleFilter = [];
public function deleteUserComments(int $userID)
#[Url(history: true)]
public $sortField = 'created_at';
#[Url(history: true)]
public $sortDirection = 'desc';
#[Url(history: true)]
public $perPage = 20;
public $selected = [];
public $selectAll = false;
public $selectPage = false;
// Modal state
public $showUserModal = false;
public $modalUser = null;
public $modalUserComments = [];
protected $queryString = [
'search' => ['except' => ''],
'discordId' => ['except' => ''],
'email' => ['except' => ''],
'roleFilter' => ['except' => []],
'sortField' => ['except' => 'created_at'],
'sortDirection' => ['except' => 'desc'],
'perPage' => ['except' => 20],
];
protected $allowedSortFields = ['id', 'name', 'email', 'discord_id', 'created_at', 'updated_at'];
protected $allowedPerPages = [10, 20, 50, 100];
public function updatedPerPage(): void
{
$user = User::where('id', $userID)
->firstOrFail();
$this->resetPage();
}
DB::table('comments')
->where('commenter_id', '=', $user->id)
->delete();
public function updatedPage(): void
{
$this->selectPage = false;
$this->selected = [];
}
public function updatedSelectPage($value): void
{
if ($value) {
$this->selected = $this->users->pluck('id')->map(fn ($id) => (string) $id)->toArray();
} else {
$this->selected = [];
}
}
public function sortBy(string $field): void
{
if (! in_array($field, $this->allowedSortFields)) {
return;
}
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function clearFilters(): void
{
$this->search = '';
$this->discordId = '';
$this->email = '';
$this->roleFilter = [];
$this->sortField = 'created_at';
$this->sortDirection = 'desc';
$this->perPage = 20;
$this->resetPage();
}
public function viewUser(int $userId): void
{
$this->modalUser = User::find($userId);
if ($this->modalUser) {
$this->modalUserComments = $this->modalUser->comments()
->orderBy('created_at', 'desc')
->limit(10)
->get();
$this->showUserModal = true;
}
}
public function closeModal(): void
{
$this->showUserModal = false;
$this->modalUser = null;
$this->modalUserComments = [];
}
public function banUser(int $userId): void
{
$user = User::findOrFail($userId);
$user->addRole(UserRole::BANNED);
cache()->flush();
$this->dispatch('notify', type: 'success', message: "{$user->name} has been banned.");
if ($this->showUserModal && $this->modalUser?->id === $userId) {
$this->modalUser->refresh();
}
}
public function unbanUser(int $userId): void
{
$user = User::findOrFail($userId);
$user->removeRole(UserRole::BANNED);
cache()->flush();
$this->dispatch('notify', type: 'success', message: "{$user->name} has been unbanned.");
if ($this->showUserModal && $this->modalUser?->id === $userId) {
$this->modalUser->refresh();
}
}
public function grantModerator(int $userId): void
{
$user = User::findOrFail($userId);
$user->addRole(UserRole::MODERATOR);
cache()->flush();
$this->dispatch('notify', type: 'success', message: "{$user->name} has been granted Moderator role.");
if ($this->showUserModal && $this->modalUser?->id === $userId) {
$this->modalUser->refresh();
}
}
public function revokeModerator(int $userId): void
{
$user = User::findOrFail($userId);
$user->removeRole(UserRole::MODERATOR);
cache()->flush();
$this->dispatch('notify', type: 'success', message: "Moderator role revoked from {$user->name}.");
if ($this->showUserModal && $this->modalUser?->id === $userId) {
$this->modalUser->refresh();
}
}
public function deleteUserComments(int $userId): void
{
$user = User::findOrFail($userId);
Comment::where('user_id', $user->id)->delete();
cache()->flush();
$this->dispatch('notify', type: 'success', message: "All comments from {$user->name} have been deleted.");
if ($this->showUserModal && $this->modalUser?->id === $userId) {
$this->modalUserComments = collect();
}
}
public function bulkBan(): void
{
$count = 0;
foreach ($this->selected as $userId) {
$user = User::find($userId);
if ($user && ! $user->hasRole(UserRole::BANNED)) {
$user->addRole(UserRole::BANNED);
$count++;
}
}
cache()->flush();
$this->selected = [];
$this->selectPage = false;
$this->dispatch('notify', type: 'success', message: "{$count} user(s) have been banned.");
}
public function bulkUnban(): void
{
$count = 0;
foreach ($this->selected as $userId) {
$user = User::find($userId);
if ($user && $user->hasRole(UserRole::BANNED)) {
$user->removeRole(UserRole::BANNED);
$count++;
}
}
cache()->flush();
$this->selected = [];
$this->selectPage = false;
$this->dispatch('notify', type: 'success', message: "{$count} user(s) have been unbanned.");
}
public function bulkDeleteComments(): void
{
$count = 0;
foreach ($this->selected as $userId) {
$deleted = Comment::where('user_id', (int) $userId)->delete();
if ($deleted > 0) {
$count++;
}
}
cache()->flush();
$this->selected = [];
$this->selectPage = false;
$this->dispatch('notify', type: 'success', message: "Deleted comments from {$count} user(s).");
}
public function getUsersProperty()
{
return User::query()
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
->when($this->email !== '', fn ($query) => $query->where('email', 'like', '%'.$this->email.'%'))
->when(! empty($this->roleFilter), function ($query) {
foreach ($this->roleFilter as $role) {
$query->whereJsonContains('roles', $role);
}
})
->orderBy($this->sortField, $this->sortDirection)
->paginate((int) $this->perPage);
}
public function render()
{
$users = User::when($this->filtered !== [], fn ($query) => $query->where('id', '>=', 10000))
->when($this->patreon !== [], fn ($query) => $query->where('is_patreon', 1))
->when($this->banned !== [], fn ($query) => $query->where('is_banned', 1))
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->paginate(20);
return view('livewire.admin-user-search', [
'users' => $users
'users' => $this->users,
]);
}
}
}
+8 -11
View File
@@ -2,12 +2,11 @@
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use App\Models\SiteBackground;
use Illuminate\Support\Carbon;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
class BackgroundImages extends Component
{
@@ -19,17 +18,15 @@ class BackgroundImages extends Component
public function render()
{
$now = Carbon::now();
$images = SiteBackground::when($this->filter === 'active', fn ($query) =>
$query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
)
->when($this->filter === 'inactive', fn ($query) =>
$query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
$images = SiteBackground::when($this->filter === 'active', fn ($query) => $query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
)
->when($this->filter === 'inactive', fn ($query) => $query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
)
->paginate(10);
return view('livewire.background-images', [
'images' => $images
'images' => $images,
]);
}
}
+59 -24
View File
@@ -1,20 +1,18 @@
<?php
namespace App\Livewire;
use App\Models\User;
use App\Enums\UserRole;
use App\Models\Episode;
use App\Models\ModLog;
use App\Models\User;
use App\Notifications\CommentNotification;
use Livewire\Component;
use Illuminate\Support\Str;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str;
use Livewire\Component;
use Maize\Markable\Models\Like;
class Comment extends Component
@@ -30,31 +28,31 @@ class Comment extends Component
public $liked = false;
public $replyState = [
'body' => ''
'body' => '',
];
public $isEditing = false;
public $editState = [
'body' => ''
'body' => '',
];
protected $listeners = [
'refresh' => '$refresh'
'refresh' => '$refresh',
];
protected $validationAttributes = [
'replyState.body' => 'reply'
'replyState.body' => 'reply',
];
public function updatedIsEditing($isEditing)
public function updatedIsEditing(bool $isEditing)
{
if (! $isEditing) {
return;
}
$this->editState = [
'body' => $this->comment->body
'body' => $this->comment->body,
];
}
@@ -71,15 +69,50 @@ class Comment extends Component
{
$this->authorize('destroy', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Deleted comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = $user->id;
$this->comment->save();
$this->dispatch('refresh');
return;
}
$this->comment->delete();
$this->dispatch('refresh');
$this->dispatch('refresh');
}
public function restoreComment()
{
$this->authorize('restore', $this->comment);
$user = Auth::user();
if ($user->hasRole(UserRole::ADMINISTRATOR) || $user->hasRole(UserRole::MODERATOR)) {
// Log to ModLog
ModLog::create([
'moderator' => $user->name,
'data' => "Restored comment {$this->comment->id} written by {$this->comment->user->id} with contents: {$this->comment->body}",
]);
$this->comment->deleted_by_moderator_id = null;
$this->comment->save();
$this->dispatch('refresh');
}
}
public function postReply()
{
if (!($this->comment->depth() < 2)) {
$this->addError('replyState.body', "Too many sub comments.");
if (! ($this->comment->depth() < 2)) {
$this->addError('replyState.body', 'Too many sub comments.');
return;
}
@@ -91,13 +124,14 @@ class Comment extends Component
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
$this->validate([
'replyState.body' => 'required'
'replyState.body' => 'required',
]);
$reply = $this->comment->children()->make($this->replyState);
@@ -110,25 +144,25 @@ class Comment extends Component
if ($reply->commentable_type == Episode::class && $user->id !== $reply->parent->user->id) {
$episode = Episode::where('id', $reply->commentable_id)
->firstOrFail();
$url = route('hentai.index', ['title' => $episode->slug]);
$reply->parent->user->notify(
new CommentNotification(
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$user->name} replied to your comment.",
Str::limit($reply->body, 50),
"{$url}#comment-{$reply->id}"
)
);
}
$this->replyState = [
'body' => ''
'body' => '',
];
$this->isReplying = false;
$this->dispatch('refresh')->self();
$this->dispatch('refresh')->self();
}
public function like()
@@ -144,6 +178,7 @@ class Comment extends Component
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
@@ -163,4 +198,4 @@ class Comment extends Component
{
return view('livewire.comment');
}
}
}
+9 -9
View File
@@ -2,11 +2,10 @@
namespace App\Livewire;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Component;
use Livewire\WithPagination;
use Illuminate\Support\Facades\RateLimiter;
class Comments extends Component
{
use WithPagination;
@@ -14,21 +13,21 @@ class Comments extends Component
public $model;
public $newCommentState = [
'body' => ''
'body' => '',
];
protected $validationAttributes = [
'newCommentState.body' => 'comment'
'newCommentState.body' => 'comment',
];
protected $listeners = [
'refresh' => '$refresh'
'refresh' => '$refresh',
];
public function postComment()
{
$this->validate([
'newCommentState.body' => 'required'
'newCommentState.body' => 'required',
]);
$user = auth()->user();
@@ -39,6 +38,7 @@ class Comments extends Component
$seconds = RateLimiter::availableIn($rateLimitKey);
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
return;
}
@@ -49,7 +49,7 @@ class Comments extends Component
$comment->save();
$this->newCommentState = [
'body' => ''
'body' => '',
];
$this->resetPage();
@@ -65,7 +65,7 @@ class Comments extends Component
->paginate(50);
return view('livewire.comments', [
'comments' => $comments
'comments' => $comments,
]);
}
}
}
+21 -2
View File
@@ -18,13 +18,32 @@ class DownloadButton extends Component
public $fillNumbers;
public $fileSize;
public $background = 'bg-rose-600';
public $fileExtension = 'HEVC';
public $version = '';
public function mount()
{
if (str_contains($this->downloadUrl, 'AV1')) {
$this->fileExtension = 'AV1';
}
if (str_contains($this->downloadUrl, 'v2')) {
$this->version = 'v2';
}
if (str_contains($this->downloadUrl, 'v3')) {
$this->version = 'v3';
}
}
public function clicked($downloadId)
{
$download = Downloads::find($downloadId);
if (!$download) {
if (! $download) {
return;
}
+6 -6
View File
@@ -5,11 +5,9 @@ namespace App\Livewire;
use App\Models\Episode;
use App\Models\User;
use App\Models\UserDownload;
use Livewire\Component;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class DownloadsFree extends Component
{
@@ -51,6 +49,7 @@ class DownloadsFree extends Component
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
// Already expired
$alreadyDownloaded->delete();
return;
}
@@ -65,6 +64,7 @@ class DownloadsFree extends Component
if ($user->downloads_left <= 0) {
// Daily limit reached
$this->granted = 3;
return;
}
@@ -75,9 +75,9 @@ class DownloadsFree extends Component
$this->granted = 1;
UserDownload::create([
'user_id' => $user->id,
'episode_id' => $this->episodeId,
'interpolated' => $this->interpolated,
'user_id' => $user->id,
'episode_id' => $this->episodeId,
'interpolated' => $this->interpolated,
]);
}
+19 -15
View File
@@ -2,10 +2,11 @@
namespace App\Livewire;
use App\Enums\UserRole;
use App\Models\Downloads;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class DownloadsSearch extends Component
{
@@ -17,7 +18,7 @@ class DownloadsSearch extends Component
public $order = 'created_at_desc';
public $options = [
'FHD' => true,
'FHD' => true,
'FHD 48fps' => true,
];
@@ -25,24 +26,25 @@ class DownloadsSearch extends Component
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
// To toggle individual option selection
public function toggleOption($option)
{
$this->options[$option] = !$this->options[$option];
$this->options[$option] = ! $this->options[$option];
$this->resetPage();
}
// To toggle dropdown visibility
public function toggleDropdown()
{
$this->isOpen = !$this->isOpen;
$this->isOpen = ! $this->isOpen;
}
protected $queryString = [
'fileSearch' => ['except' => '', 'as' => 'fS'],
'order' => ['except' => '', 'as' => 'order'],
'fileSearch' => ['except' => '', 'as' => 'fS'],
'order' => ['except' => '', 'as' => 'order'],
];
public function updatingFileSearch()
@@ -73,9 +75,9 @@ class DownloadsSearch extends Component
$types[] = 'FHD';
} elseif ($label === 'FHD 48fps') {
$types[] = 'FHDi';
} elseif ($label === 'UHD' && auth()->user()->is_patreon) {
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHD';
} elseif ($label === 'UHD 48fps' && auth()->user()->is_patreon) {
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
$types[] = 'UHDi';
}
}
@@ -87,7 +89,7 @@ class DownloadsSearch extends Component
public function clicked($downloadId)
{
$download = Downloads::find($downloadId);
if (!$download) {
if (! $download) {
return;
}
@@ -96,9 +98,9 @@ class DownloadsSearch extends Component
cache()->forget("episode_{$download->episode->id}_download_{$download->type}");
}
public function mount()
public function mount()
{
if (!auth()->user()->is_patreon) {
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
return;
}
@@ -144,15 +146,17 @@ class DownloadsSearch extends Component
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
->whereIn('type', $this->getSelectedTypes())
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); })))
->when($this->studios !== [], fn ($q) => $q->whereHas('episode', fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
})))
->whereNotNull('size')
->orderBy($orderby, $orderdirection)
->paginate(20);
return view('livewire.downloads-search', [
'downloads' => $downloads,
'query' => $this->fileSearch,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'downloads' => $downloads,
'query' => $this->fileSearch,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
]);
}
}
+2 -2
View File
@@ -4,10 +4,9 @@ namespace App\Livewire;
use App\Models\Episode;
use App\Models\User;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Livewire\Component;
use Maize\Markable\Models\Like;
class LikeButton extends Component
@@ -52,6 +51,7 @@ class LikeButton extends Component
if ($this->liked) {
$this->liked = false;
$this->likeCount--;
return;
}
+24 -17
View File
@@ -3,10 +3,10 @@
namespace App\Livewire;
use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class LiveSearch extends Component
{
@@ -20,14 +20,17 @@ class LiveSearch extends Component
#[Url(history: true)]
public $tags = [];
public $tagsCopy = [];
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
#[Url(history: true)]
public $blacklist = [];
public $blacklistCopy = [];
#[Url(history: true)]
@@ -72,7 +75,7 @@ class LiveSearch extends Component
public function mount()
{
// User blacklist
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
$this->blacklist = auth()->user()->tag_blacklist;
}
@@ -118,11 +121,15 @@ class LiveSearch extends Component
}
$user_id = Auth::check() ? auth()->user()->id : 0;
$episodes = Episode::with('gallery')->when($this->search != '', fn ($query) => $query->where(function($query) { $query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%'); }))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); }))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$episodes = Episode::with('gallery')->when($this->search != '', fn ($query) => $query->where(function ($query) {
$query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%');
}))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
}))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id);
}))
->when(Auth::guest(), fn ($query) => $query->withoutTags(['loli', 'shota']))
@@ -140,16 +147,16 @@ class LiveSearch extends Component
$this->dispatch('contentChanged');
return view('livewire.live-search', [
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'selectedblacklist' => $this->blacklist,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
]);
}
}
+5 -4
View File
@@ -3,8 +3,8 @@
namespace App\Livewire;
use App\Models\Episode;
use Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class NavLiveSearch extends Component
{
@@ -20,14 +20,15 @@ class NavLiveSearch extends Component
if ($this->navSearch != '') {
$episodes = Episode::search($this->navSearch)
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
->query(fn ($query) => $query->with(['gallery', 'studio']))
->take(7)
->get();
}
return view('livewire.nav-live-search', [
'episodes' => $episodes,
'query' => $this->navSearch,
'hide' => empty($this->navSearch),
'episodes' => $episodes,
'query' => $this->navSearch,
'hide' => empty($this->navSearch),
]);
}
}
+54 -5
View File
@@ -5,13 +5,11 @@ namespace App\Livewire;
use App\Models\Playlist;
use App\Models\PlaylistEpisode;
use App\Services\PlaylistService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Collection;
class PlaylistOverview extends Component
{
@@ -28,6 +26,10 @@ class PlaylistOverview extends Component
public Collection $playlistEpisodes;
public bool $editingName = false;
public string $editingPlaylistName = '';
public function boot(PlaylistService $playlistService)
{
$this->playlistService = $playlistService;
@@ -114,6 +116,53 @@ class PlaylistOverview extends Component
$this->refreshEpisodes();
}
public function editName()
{
if (! Auth::check() || Auth::user()->id !== $this->playlist->user->id) {
return;
}
$this->editingPlaylistName = $this->playlist->name;
$this->editingName = true;
}
public function cancelEditName()
{
$this->editingName = false;
$this->editingPlaylistName = '';
}
public function updateName()
{
if (! Auth::check() || Auth::user()->id !== $this->playlist->user->id) {
return;
}
$this->validate([
'editingPlaylistName' => 'required|max:30',
]);
$this->playlist->update([
'name' => $this->editingPlaylistName,
]);
$this->editingName = false;
$this->editingPlaylistName = '';
}
public function toggleVisibility()
{
if (! Auth::check() || Auth::user()->id !== $this->playlist->user->id) {
return;
}
$this->playlist->update([
'is_private' => ! $this->playlist->is_private,
]);
$this->playlist->refresh();
}
public function render()
{
return view('livewire.playlist-overview', [
+3 -4
View File
@@ -3,10 +3,9 @@
namespace App\Livewire;
use App\Models\Playlist;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class Playlists extends Component
{
@@ -54,12 +53,12 @@ class Playlists extends Component
$playlists = Playlist::where('is_private', 0)
->withCount('episodes')
->having('episodes_count', '>', 1)
->when($this->search != '', fn($query) => $query->where('name', 'like', '%' . $this->search . '%'))
->when($this->search != '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
->orderBy($orderby, $orderdirection)
->paginate($this->pagination);
return view('livewire.playlists', [
'playlists' => $playlists
'playlists' => $playlists,
]);
}
}
+2 -4
View File
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\Comment;
use Livewire\Component;
use Livewire\WithPagination;
@@ -36,14 +35,13 @@ class UserComments extends Component
$orderdirection = 'desc';
}
$comments = Comment::where('user_id', $this->model->id)
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
->orderBy($orderby, $orderdirection)
->paginate(10);
->paginate(6);
return view('livewire.user-comments', [
'comments' => $comments
'comments' => $comments,
]);
}
}
+24 -17
View File
@@ -3,10 +3,10 @@
namespace App\Livewire;
use App\Models\Episode;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;
use Illuminate\Support\Facades\Auth;
class UserLikes extends Component
{
@@ -20,14 +20,17 @@ class UserLikes extends Component
#[Url(history: true)]
public $tags = [];
public $tagsCopy = [];
#[Url(history: true)]
public $studios = [];
public $studiosCopy = [];
#[Url(history: true)]
public $blacklist = [];
public $blacklistCopy = [];
#[Url(history: true)]
@@ -72,7 +75,7 @@ class UserLikes extends Component
public function mount()
{
// User blacklist
if (Auth::check() && empty($this->blacklist) && !empty(auth()->user()->tag_blacklist)) {
if (Auth::check() && empty($this->blacklist) && ! empty(auth()->user()->tag_blacklist)) {
$this->blacklist = auth()->user()->tag_blacklist;
}
@@ -119,11 +122,15 @@ class UserLikes extends Component
$user_id = Auth::check() ? auth()->user()->id : 0;
$episodes = Episode::whereHasLike(auth()->user())
->when($this->search !== '', fn ($query) => $query->where(function($query) { $query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%'); }))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) { $query->whereIn('slug', $this->studios); }))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
->when($this->search !== '', fn ($query) => $query->where(function ($query) {
$query->where('title', 'like', '%'.$this->search.'%')->orWhere('title_search', 'like', '%'.$this->search.'%')->orWhere('title_jpn', 'like', '%'.$this->search.'%');
}))
->when($this->tags !== [], fn ($query) => $query->withAllTags($this->tags))
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
->when($this->studios !== [], fn ($query) => $query->whereHas('studio', function ($query) {
$query->whereIn('slug', $this->studios);
}))
->when($this->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
$query->where('user_id', $user_id);
}))
->orderBy($orderby, $orderdirection)
@@ -137,16 +144,16 @@ class UserLikes extends Component
}
return view('livewire.user-likes', [
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'episodes' => $episodes,
'tagcount' => is_array($this->tags) ? count($this->tags) : 0,
'studiocount' => is_array($this->studios) ? count($this->studios) : 0,
'blacklistcount' => is_array($this->blacklist) ? count($this->blacklist) : 0,
'query' => $this->search,
'selectedtags' => $this->tags,
'selectedstudios' => $this->studios,
'selectedblacklist' => $this->blacklist,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
'searchIsJpn' => $searchIsJpn,
'view' => $this->view,
]);
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Episode;
use Livewire\Component;
class ViewCount extends Component
{
public $episodeId = 0;
public $viewCount = 0;
public function mount(Episode $episode)
{
$this->episodeId = $episode->id;
$this->viewCount = $episode->view_count;
}
public function update()
{
$this->viewCount = Episode::where('id', $this->episodeId)->firstOrFail()->view_count;
}
public function render()
{
return view('livewire.view-count');
}
}
+3 -4
View File
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\Watched as UserWatched;
use App\Models\User;
use Illuminate\Support\Carbon;
use Livewire\Component;
use Livewire\WithPagination;
@@ -14,7 +13,7 @@ class Watched extends Component
public $userId;
public function mount($user)
public function mount($user)
{
$this->userId = $user ? $user->id : auth()->user()->id;
}
@@ -27,8 +26,8 @@ class Watched extends Component
});
return view('livewire.watched', [
'watched' => $watched,
'watchedGrouped' => $watchedGrouped,
'watched' => $watched,
'watchedGrouped' => $watchedGrouped,
]);
}
}
+14 -7
View File
@@ -4,19 +4,18 @@ namespace App\Models;
use App\Models\Presenters\CommentPresenter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
class Comment extends Model
{
use HasFactory, SoftDeletes, Markable;
use HasFactory, Markable, SoftDeletes;
protected static $marks = [
Like::class
Like::class,
];
/**
@@ -25,7 +24,7 @@ class Comment extends Model
* @var string[]
*/
protected $fillable = [
'body'
'body',
];
public function presenter()
@@ -71,6 +70,14 @@ class Comment extends Model
*/
public function likeCount(): int
{
return cache()->remember('commentLikes' . $this->id, 300, fn() => $this->likes->count());
return cache()->remember('commentLikes'.$this->id, 300, fn () => $this->likes->count());
}
/**
* Returns wether or not comment has been removed by moderation
*/
public function isDeletedByModerator(): bool
{
return $this->deleted_by_moderator_id !== null;
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ class Downloads extends Model
$bytes /= 1024;
}
return round($bytes, 2) . ' ' . $units[$i];
return round($bytes, 2).' '.$units[$i];
}
/**
+30 -35
View File
@@ -2,35 +2,28 @@
namespace App\Models;
use App\Models\Downloads;
use App\Models\PopularMonthly;
use App\Models\PopularWeekly;
use App\Models\PopularDaily;
use Conner\Tagging\Taggable;
use Laravel\Scout\Searchable;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Laravel\Scout\Searchable;
use Maize\Markable\Markable;
use Maize\Markable\Models\Like;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
class Episode extends Model implements Sitemapable
{
use Markable, Taggable;
use HasFactory;
use Markable, Taggable;
use Searchable;
protected static $marks = [
Like::class
Like::class,
];
/**
@@ -49,14 +42,14 @@ class Episode extends Model implements Sitemapable
public function toSearchableArray()
{
return [
'title' => $this->title,
'title_search' => $this->title_search,
'title_jpn' => $this->title_jpn,
'slug' => $this->slug,
'description' => $this->description,
'tags' => $this->tagNames(),
'release_date' => $this->release_date,
'created_at' => $this->created_at,
'title' => $this->title,
'title_search' => $this->title_search,
'title_jpn' => $this->title_jpn,
'slug' => $this->slug,
'description' => $this->description,
'tags' => $this->tagNames(),
'release_date' => $this->release_date,
'created_at' => $this->created_at,
];
}
@@ -125,7 +118,7 @@ class Episode extends Model implements Sitemapable
*/
public function viewCount(): int
{
return cache()->remember('episodeViews' . $this->id, 300, fn() => $this->view_count);
return cache()->remember('episodeViews'.$this->id, 300, fn () => $this->view_count);
}
/**
@@ -141,7 +134,7 @@ class Episode extends Model implements Sitemapable
$index = floor(log($this->viewCount(), 1000));
$shortNumber = $this->viewCount() / pow(1000, $index);
return round($shortNumber, 0) . $units[$index - 1];
return round($shortNumber, 0).$units[$index - 1];
}
/**
@@ -149,7 +142,7 @@ class Episode extends Model implements Sitemapable
*/
public function likeCount(): int
{
return cache()->remember('episodeLikes' . $this->id, 300, fn() => $this->likes->count());
return cache()->remember('episodeLikes'.$this->id, 300, fn () => $this->likes->count());
}
/**
@@ -157,7 +150,7 @@ class Episode extends Model implements Sitemapable
*/
public function commentCount(): int
{
return cache()->remember('episodeComments' . $this->id, 300, fn() => $this->comments->count());
return cache()->remember('episodeComments'.$this->id, 300, fn () => $this->comments->count());
}
public function comments()
@@ -180,6 +173,7 @@ class Episode extends Model implements Sitemapable
$problematicResults .= $pTag;
}
return $problematicResults;
}
@@ -202,17 +196,17 @@ class Episode extends Model implements Sitemapable
*/
public function hasAutoTrans(): bool
{
return cache()->remember('mt' . $this->id, 900, fn() => $this->subtitles()->exists());
return cache()->remember('mt'.$this->id, 900, fn () => $this->subtitles()->exists());
}
public function is48Fps(): bool
{
return cache()->remember('48fps' . $this->id, 900, fn() => $this->interpolated);
return cache()->remember('48fps'.$this->id, 900, fn () => $this->interpolated);
}
public function isUHD48Fps(): bool
{
return cache()->remember('48fpsUHD' . $this->id, 900, fn() => $this->interpolated_uhd);
return cache()->remember('48fpsUHD'.$this->id, 900, fn () => $this->interpolated_uhd);
}
public function getResolution(): string
@@ -226,7 +220,7 @@ class Episode extends Model implements Sitemapable
public function userWatched(int $user_id): bool
{
return cache()->remember('user' . $user_id . 'watched' . $this->id, 300, fn() => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
return cache()->remember('user'.$user_id.'watched'.$this->id, 300, fn () => Watched::where('user_id', $user_id)->where('episode_id', $this->id)->exists());
}
public function watched(): HasMany
@@ -234,15 +228,16 @@ class Episode extends Model implements Sitemapable
return $this->hasMany(Watched::class);
}
public function getDownloadByType(string $type): Downloads | null
public function getDownloadByType(string $type): ?Downloads
{
$cacheKey = "episode_{$this->id}_download_{$type}";
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) {
return $this->downloads()->where('type', $type)->first();
});
}
public function toSitemapTag(): Url | string | array
public function toSitemapTag(): Url|string|array
{
return Url::create(route('hentai.index', $this->slug))
->setLastModificationDate(Carbon::create($this->created_at));
+8 -9
View File
@@ -2,19 +2,18 @@
namespace App\Models;
use Conner\Tagging\Taggable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Spatie\Sitemap\Contracts\Sitemapable;
use Spatie\Sitemap\Tags\Url;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Conner\Tagging\Taggable;
class Hentai extends Model implements Sitemapable
{
use Taggable;
use HasFactory;
use Taggable;
/**
* The attributes that are mass assignable.
@@ -31,7 +30,7 @@ class Hentai extends Model implements Sitemapable
return $this->hasMany(Episode::class, 'hentai_id');
}
public function title(): String
public function title(): string
{
return $this->episodes->first()->title;
}
@@ -63,7 +62,7 @@ class Hentai extends Model implements Sitemapable
);
}
public function toSitemapTag(): Url | string | array
public function toSitemapTag(): Url|string|array
{
return Url::create(route('hentai.index', $this->slug))
->setLastModificationDate(Carbon::create($this->created_at));
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ModLog extends Model
{
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'moderator',
'data',
];
}
+21 -1
View File
@@ -2,11 +2,31 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Playlist extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'name',
'is_private',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'is_private' => 'boolean',
];
/**
* Belongs To A User.
*/
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PlaylistEpisode extends Model
{
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularDaily extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularMonthly extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -13,7 +13,7 @@ class PopularWeekly extends Model
*
* @var string[]
*/
protected $fillable = [ 'episode_id' ];
protected $fillable = ['episode_id'];
/**
* Get the Episode.
+1 -1
View File
@@ -25,4 +25,4 @@ class CommentPresenter
{
return $this->comment->created_at->diffForHumans();
}
}
}
+3 -3
View File
@@ -16,19 +16,19 @@ class SiteBackground extends Model
protected $fillable = [
'date_start',
'date_end',
'default'
'default',
];
/**
* Returns the current IDs of active wallpaper
*/
public function getImages(): ? \Illuminate\Support\Collection
public function getImages(): ?Collection
{
$now = Carbon::now();
$byDates = $this->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)->get()->pluck('id');
$default = $this->where('default', true)->get()->pluck('id');
return $byDates->isEmpty() ? $default : $byDates;
}
}
+2 -2
View File
@@ -3,13 +3,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Studios extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
+57 -14
View File
@@ -2,18 +2,21 @@
namespace App\Models;
//use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Relations\HasMany;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\UserRole;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\LaravelPasskeys\Models\Concerns\HasPasskeys;
use Spatie\LaravelPasskeys\Models\Concerns\InteractsWithPasskeys;
class User extends Authenticatable
class User extends Authenticatable implements HasPasskeys
{
use HasFactory, Notifiable;
use HasFactory, InteractsWithPasskeys, Notifiable;
/**
* The attributes that are mass assignable.
@@ -25,7 +28,6 @@ class User extends Authenticatable
'email',
'password',
'locale',
'is_banned',
// Discord
'discord_id',
'discord_avatar',
@@ -54,14 +56,13 @@ class User extends Authenticatable
'name' => 'string',
'email' => 'string',
'locale' => 'string',
'roles' => 'json',
'roles' => 'array',
'tag_blacklist' => 'array',
// Discord
'discord_id' => 'integer',
'discord_avatar' => 'string',
];
/**
* Has Many Playlists.
*/
@@ -99,7 +100,7 @@ class User extends Authenticatable
*/
public function commentCount(): int
{
return cache()->remember('userComments' . $this->id, 300, fn() => $this->comments->count());
return cache()->remember('userComments'.$this->id, 300, fn () => $this->comments->count());
}
/**
@@ -107,16 +108,58 @@ class User extends Authenticatable
*/
public function getAvatar(): string
{
if ($this->discord_id && $this->discord_avatar && !$this->avatar)
{
if ($this->discord_id && $this->discord_avatar && ! $this->avatar) {
return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}";
}
if ($this->avatar)
{
if ($this->avatar) {
return Storage::url($this->avatar);
}
return asset('images/default-avatar.webp');
}
/**
* Check if user has a specific role
*/
public function hasRole(UserRole $role): bool
{
return in_array($role->value, $this->roles ?? [], true);
}
/**
* Add Role to User
*/
public function addRole(UserRole $role): void
{
if ($this->hasRole($role)) {
return;
}
// Get all current roles
$roles = $this->roles ?? [];
// Add new role
$roles[] = $role->value;
$this->roles = $roles;
$this->save();
}
/**
* Remove Role from User
*/
public function removeRole(UserRole $role): void
{
if (! $this->hasRole($role)) {
return;
}
$this->roles = collect($this->roles)
->reject(fn ($value) => $value === $role->value)
->values()
->all();
$this->save();
}
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserDownload extends Model
{
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VideoEngagement extends Model
{
public $table = 'video_engagement';
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = ['episode_id', 'user_id', 'segment'];
/**
* Get the Episode.
*/
public function episode(): BelongsTo
{
return $this->belongsTo(Episode::class, 'episode_id');
}
/**
* Get the User.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}
+1 -1
View File
@@ -2,8 +2,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Watched extends Model
{
+5 -5
View File
@@ -3,8 +3,6 @@
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class CommentNotification extends Notification
@@ -12,7 +10,9 @@ class CommentNotification extends Notification
use Queueable;
protected $type;
protected $message;
protected $url;
/**
@@ -38,9 +38,9 @@ class CommentNotification extends Notification
public function toDatabase($notifiable)
{
return [
'type' => $this->type,
'message' => $this->message,
'url' => $this->url,
'type' => $this->type,
'message' => $this->message,
'url' => $this->url,
];
}
}
+23 -2
View File
@@ -2,8 +2,9 @@
namespace App\Policies;
use App\Models\User;
use App\Enums\UserRole;
use App\Models\Comment;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class CommentPolicy
@@ -17,6 +18,26 @@ class CommentPolicy
public function destroy(User $user, Comment $comment): bool
{
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return $user->id === $comment->user_id;
}
}
public function restore(User $user, Comment $comment): bool
{
// Comment not deleted
if ($comment->deleted_by_moderator_id === null) {
return false;
}
if ($user->hasRole(UserRole::ADMINISTRATOR) ||
$user->hasRole(UserRole::MODERATOR)) {
return true;
}
return false;
}
}
+4 -2
View File
@@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use SocialiteProviders\Discord\Provider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class AppServiceProvider extends ServiceProvider
{
@@ -20,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
Event::listen(function (SocialiteWasCalled $event) {
$event->extendSocialite('discord', Provider::class);
});
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Rules;
use AltchaOrg\Altcha\Algorithm\Pbkdf2;
use AltchaOrg\Altcha\Altcha;
use AltchaOrg\Altcha\Challenge;
use AltchaOrg\Altcha\ChallengeParameters;
use AltchaOrg\Altcha\Payload;
use AltchaOrg\Altcha\Solution;
use AltchaOrg\Altcha\VerifySolutionOptions;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Translation\PotentiallyTranslatedString;
/**
* Validation rule to verify captcha solution.
*/
class ValidCaptcha implements ValidationRule
{
/**
* Altcha instance.
*/
protected Altcha $altcha;
/**
* Pbkdf2 algorithm instance.
*/
protected Pbkdf2 $pbkdf2;
/**
* Class constructor.
*/
public function __construct()
{
$this->pbkdf2 = new Pbkdf2;
$this->altcha = new Altcha(
hmacSignatureSecret: config('captcha.hmac_key'),
);
}
/**
* Parse payload and return the decoded data as an array.
*/
private function parsePayload(string $value): ?array
{
$decoded = base64_decode($value, true);
if ($decoded === false) {
return null;
}
$payload = json_decode($decoded, true);
if (! is_array($payload)) {
return null;
}
return $payload;
}
/**
* Verify if payload has required fields.
*/
private function verifyFields(array $payload): bool
{
if (! isset($payload['challenge'], $payload['solution'])) {
return false;
}
if (! is_array($payload['challenge']) || ! is_array($payload['solution'])) {
return false;
}
return true;
}
/**
* Create a Challenge object from challenge data.
*/
private function createChallenge(array $challengeData): Challenge
{
return new Challenge(
ChallengeParameters::fromArray($challengeData['parameters'] ?? []),
$challengeData['signature'] ?? null,
);
}
/**
* Create a Solution object from solution data.
*/
private function createSolution(array $solutionData): Solution
{
return new Solution(
counter: (int) ($solutionData['counter'] ?? 0),
derivedKey: (string) ($solutionData['derivedKey'] ?? ''),
);
}
/**
* Run the validation rule.
*
* @param Closure(string, ?string=): PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$payload = $this->parsePayload($value);
if (! $payload) {
$fail('Invalid captcha.');
return;
}
if (! $this->verifyFields($payload)) {
$fail('Invalid captcha.');
return;
}
$challenge = $this->createChallenge($payload['challenge']);
$solution = $this->createSolution($payload['solution']);
$result = $this->altcha->verifySolution(new VerifySolutionOptions(
algorithm: $this->pbkdf2,
payload: new Payload($challenge, $solution),
));
if (! $result->verified) {
$fail('Invalid captcha.');
}
}
}
+7 -9
View File
@@ -2,11 +2,9 @@
namespace App\Services;
use App\Jobs\GetFileSizeFromCDN;
use App\Models\Downloads;
use App\Models\Episode;
use App\Jobs\GetFileSizeFromCDN;
use Illuminate\Http\Request;
class DownloadService
@@ -14,10 +12,10 @@ class DownloadService
public function createOrUpdateDownloads(Request $request, Episode $episode, int $index): void
{
$downloadTypes = [
'episodedlurl' => 'FHD',
'episodedlurlinterpolated' => 'FHDi',
'episodedlurl4k' => 'UHD',
'downloadUHDi' => 'UHDi',
'episodedlurl' => 'FHD',
'episodedlurlinterpolated' => 'FHDi',
'episodedlurl4k' => 'UHD',
'downloadUHDi' => 'UHDi',
];
foreach ($downloadTypes as $inputField => $type) {
@@ -25,9 +23,9 @@ class DownloadService
if ($request->filled($fieldName)) {
$download = Downloads::updateOrCreate([
'episode_id' => $episode->id,
'type' => $type,
'type' => $type,
], [
'url' => $request->input($fieldName),
'url' => $request->input($fieldName),
]);
// Dispatch Job to get File Size from CDN
+94 -20
View File
@@ -5,14 +5,13 @@ namespace App\Services;
use App\Models\Episode;
use App\Models\Hentai;
use App\Models\Studios;
use App\Models\ModLog;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\Support\Str;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class EpisodeService
{
@@ -24,6 +23,7 @@ class EpisodeService
if (is_numeric($lastPart) && $lastPart < 1000) {
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
return implode('-', $slugParts);
}
@@ -31,16 +31,15 @@ class EpisodeService
}
public function createEpisode(
Request $request,
Request $request,
Hentai $hentai,
int $episodeNumber,
Studios $studio = null,
Episode $referenceEpisode = null
): Episode
{
$episode = new Episode();
?Studios $studio = null,
?Episode $referenceEpisode = null
): Episode {
$episode = new Episode;
$episode->title = $referenceEpisode->title ?? $request->input('title');
$episode->title_search = preg_replace("/[^A-Za-z0-9 ]/", '', $episode->title);
$episode->title_search = preg_replace('/[^A-Za-z0-9 ]/', '', $episode->title);
$episode->title_jpn = $referenceEpisode->title_jpn ?? $request->input('title_jpn');
$episode->slug = "{$hentai->slug}-{$episodeNumber}";
$episode->hentai_id = $hentai->id;
@@ -64,6 +63,68 @@ class EpisodeService
return $episode;
}
private function applyTags(Request $request, Episode $episode): void
{
$tags = json_decode($request->input('tags'));
$newtags = [];
foreach ($tags as $t) {
$newtags[] = $t->value;
}
$newTagsTemp = $newtags;
$oldTagsTemp = $episode->tagNames();
sort($newTagsTemp);
sort($oldTagsTemp);
if ($newTagsTemp !== $oldTagsTemp) {
ModLog::create([
'moderator' => $request->user()->name,
'data' => sprintf(
'Updated Episode tags from %s to %s',
implode(', ', $oldTagsTemp),
implode(', ', $newTagsTemp),
),
]);
}
$episode->retag($newtags);
}
private function updateTitle(Request $request, Episode $episode): void
{
$updates = [];
if ($episode->title !== $request->input('title')) {
$updates['title'] = $request->input('title');
$updates['title_search'] = preg_replace(
'/[^A-Za-z0-9 ]/',
'',
$request->input('title')
);
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updating Hentai Title from {$episode->title} to {$request->input('title')}",
]);
}
if ($episode->title_jpn !== $request->input('title_jpn')) {
$updates['title_jpn'] = $request->input('title_jpn');
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updating Hentai Title from {$episode->title_jpn} to {$request->input('title_jpn')}",
]);
}
if (! empty($updates)) {
$episode->hentai->episodes()->update($updates);
}
}
public function updateEpisode(Request $request, Studios $studio, int $episodeId): Episode
{
$episode = Episode::where('id', $episodeId)->firstOrFail();
@@ -77,17 +138,31 @@ class EpisodeService
$episode->dmca_takedown = $request->input('dmca_takedown') == 'true';
$episode->save();
// Tagging
$tags = json_decode($request->input('tags'));
$newtags = [];
foreach ($tags as $t) {
$newtags[] = $t->value;
}
$episode->retag($newtags);
$this->applyTags($request, $episode);
$this->updateTitle($request, $episode);
return $episode;
}
public function updateEpisodeModerator(Request $request, int $episodeId): void
{
$episode = Episode::where('id', $episodeId)->firstOrFail();
$oldDescription = $episode->description;
$episode->description = $request->input('description');
$episode->save();
if ($episode->description !== $oldDescription) {
// Log to ModLog
ModLog::create([
'moderator' => $request->user()->name,
'data' => "Updated Episode description from {$oldDescription} to {$episode->description}",
]);
}
$this->applyTags($request, $episode);
$this->updateTitle($request, $episode);
}
public function getOrCreateStudio(string $studioName): Studios
{
return Studios::firstOrCreate(
@@ -96,7 +171,6 @@ class EpisodeService
);
}
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
{
if (! $request->hasFile("episodecover{$episodeNumber}")) {
@@ -111,7 +185,7 @@ class EpisodeService
// Encode and save cover image
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
->cover(268, 394)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($episode->cover_url));
}
}
+6 -10
View File
@@ -5,29 +5,25 @@ namespace App\Services;
use App\Models\Episode;
use App\Models\Gallery;
use App\Models\Hentai;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Laravel\Facades\Image;
use Intervention\Image\Encoders\WebpEncoder;
use Intervention\Image\Laravel\Facades\Image;
class GalleryService
{
public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void
{
$galleryInputNumber = $override ? 1 : $episodeNumber;
if($request->hasFile('episodegallery'.$galleryInputNumber)) {
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
$this->deleteOldGallery($episode);
$this->createGalleryFolder($hentai);
$counter = 0;
foreach($request->file('episodegallery'.$galleryInputNumber) as $file) {
foreach ($request->file('episodegallery'.$galleryInputNumber) as $file) {
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
$this->saveGalleryImage($gallery, $file);
$counter += 1;
@@ -45,7 +41,7 @@ class GalleryService
private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery
{
$gallery = new Gallery();
$gallery = new Gallery;
$gallery->hentai_id = $hentai->id;
$gallery->episode_id = $episode->id;
$gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp";
@@ -59,12 +55,12 @@ class GalleryService
{
Image::read($sourceImage->getRealPath())
->cover(1920, 1080)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->image_url));
Image::read($sourceImage->getRealPath())
->cover(960, 540)
->encode(new WebpEncoder())
->encode(new WebpEncoder)
->save(Storage::disk('public')->path($gallery->thumbnail_url));
}
@@ -0,0 +1,49 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class MatrixRegistrationService
{
public function registerUser(string $username, string $password)
{
$server = config('services.matrix.server');
$secret = config('services.matrix.shared_secret');
// Get nonce from Synapse
$nonceResponse = Http::get("$server/_synapse/admin/v1/register");
if (! $nonceResponse->ok()) {
throw new \Exception('Could not fetch nonce from Matrix.');
}
$nonce = $nonceResponse->json()['nonce'];
// Generate MAC
$mac = hash_hmac(
'sha1',
$nonce."\0".
$username."\0".
$password."\0".
'notadmin',
$secret
);
// Send registration request
$response = Http::post("$server/_synapse/admin/v1/register", [
'nonce' => $nonce,
'username' => $username,
'password' => $password,
'admin' => false,
'mac' => $mac,
]);
if ($response->failed()) {
$error = $response->json()['error'] ?? $response->body();
throw new \Exception($error);
}
return $response->json();
}
}
+9 -4
View File
@@ -1,5 +1,10 @@
<?php
use App\Exceptions\Handler;
use App\Http\Kernel;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Foundation\Application;
/*
|--------------------------------------------------------------------------
| Create The Application
@@ -11,7 +16,7 @@
|
*/
$app = new Illuminate\Foundation\Application(
$app = new Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
Kernel::class
);
$app->singleton(
@@ -37,8 +42,8 @@ $app->singleton(
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
ExceptionHandler::class,
Handler::class
);
/*
+14 -20
View File
@@ -1,7 +1,7 @@
{
"name": "laravel/laravel",
"name": "w33b/hstream",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"description": "The website of hstream.moe",
"keywords": [
"laravel",
"framework"
@@ -9,45 +9,39 @@
"license": "MIT",
"require": {
"php": "^8.2",
"altcha-org/altcha": "^2.0",
"guzzlehttp/guzzle": "^7.8.1",
"hisorange/browser-detect": "^5.0",
"http-interop/http-factory-guzzle": "^1.2",
"intervention/image": "^3.9",
"intervention/image-laravel": "^1.3",
"laravel/framework": "^11.0",
"laravel/sanctum": "^4.0",
"intervention/image": "^3.11",
"intervention/image-laravel": "^1.5",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/scout": "^10.20",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.10",
"livewire/livewire": "^3.6.4",
"livewire/livewire": "^3.7.0",
"maize-tech/laravel-markable": "^2.3.0",
"meilisearch/meilisearch-php": "^1.16",
"mews/captcha": "3.4.4",
"predis/predis": "^2.2",
"realrashid/sweet-alert": "^7.2",
"rtconner/laravel-tagging": "^4.1",
"rtconner/laravel-tagging": "^5.0",
"socialiteproviders/discord": "^4.2",
"spatie/laravel-discord-alerts": "^1.5",
"spatie/laravel-sitemap": "^7.3",
"vluzrmos/language-detector": "^2.3"
"spatie/laravel-discord-alerts": "^1.8",
"spatie/laravel-passkeys": "^1.7",
"spatie/laravel-sitemap": "^7.3"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.14.7",
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.24.0",
"laravel/breeze": "^2.3",
"laravel/pint": "^1.18",
"laravel/sail": "^1.38",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.4",
"spatie/laravel-ignition": "^2.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/renatokira/comments.git"
}
],
"repositories": [],
"autoload": {
"exclude-from-classmap": [],
"psr-4": {
Generated
+2310 -1389
View File
File diff suppressed because it is too large Load Diff
+22 -5
View File
@@ -1,7 +1,12 @@
<?php
use App\Providers\AppServiceProvider;
use App\Providers\AuthServiceProvider;
use App\Providers\EventServiceProvider;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\ServiceProvider;
use Mews\Captcha\Facades\Captcha;
return [
@@ -111,6 +116,18 @@ return [
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Supported Locales
|--------------------------------------------------------------------------
|
| This is used to display the supported locales by this app, it also is
| used to verify session data and requests in the SetLocale Middleware
|
*/
'supported_locales' => ['en', 'de', 'fr'],
/*
|--------------------------------------------------------------------------
| Encryption Key
@@ -163,11 +180,11 @@ return [
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
AppServiceProvider::class,
AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
EventServiceProvider::class,
RouteServiceProvider::class,
])->toArray(),
/*
@@ -183,7 +200,7 @@ return [
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
'Captcha' => Mews\Captcha\Facades\Captcha::class,
'Captcha' => Captcha::class,
])->toArray(),
];
+3 -1
View File
@@ -1,5 +1,7 @@
<?php
use App\Models\User;
return [
/*
@@ -62,7 +64,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
'model' => User::class,
],
// 'users' => [
+5 -46
View File
@@ -1,50 +1,9 @@
<?php
return [
'characters' => ['2', '3', '4', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'X', 'Y', 'Z'],
'default' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => false,
'expire' => 60,
'encrypt' => false,
],
'math' => [
'length' => 9,
'width' => 120,
'height' => 36,
'quality' => 90,
'math' => true,
],
/**
* Altcha Captcha System
*/
'flat' => [
'length' => 6,
'width' => 160,
'height' => 46,
'quality' => 90,
'lines' => 6,
'bgImage' => false,
'bgColor' => '#ecf2f4',
'fontColors' => ['#2c3e50', '#c0392b', '#16a085', '#c0392b', '#8e44ad', '#303f9f', '#f57c00', '#795548'],
'contrast' => -5,
],
'mini' => [
'length' => 3,
'width' => 60,
'height' => 32,
],
'inverse' => [
'length' => 5,
'width' => 120,
'height' => 36,
'quality' => 90,
'sensitive' => true,
'angle' => 12,
'sharpen' => 10,
'blur' => 2,
'invert' => true,
'contrast' => -5,
]
return [
'hmac_key' => env('ALTCHA_HMAC_KEY'),
];
+5 -3
View File
@@ -1,12 +1,14 @@
<?php
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;
return [
/*
* The webhook URLs that we'll use to send a message to Discord.
*/
'webhook_urls' => [
'default' => env('DISCORD_ALERT_WEBHOOK'),
'update' => env('DISCORD_ALERT_UPDATE_WEBHOOK'),
'default' => env('DISCORD_ALERT_WEBHOOK'),
'update' => env('DISCORD_ALERT_UPDATE_WEBHOOK'),
'rerelease' => env('DISCORD_ALERT_RERELEASE_WEBHOOK'),
],
@@ -14,5 +16,5 @@ return [
* This job will send the message to Discord. You can extend this
* job to set timeouts, retries, etc...
*/
'job' => Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob::class,
'job' => SendToDiscordChannelJob::class,
];
+9 -9
View File
@@ -1,18 +1,18 @@
<?php
return [
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
'invite_link' => 'https://discord.gg/yAqgVKNgG5',
'guild_id' => 802233383710228550,
'guild_id' => 802233383710228550,
'patreon_roles' => [
841798154999169054, // ????
803329707650187364, // Tier-5
803327903659196416, // ????
803325441942356059, // Tier-3
803322725576736858, // Tier-2
802270568912519198, // Tier-1
802234830384267315 // admin
'841798154999169054', // ????
'803329707650187364', // Tier-5
'803327903659196416', // ????
'803325441942356059', // Tier-3
'803322725576736858', // Tier-2
'802270568912519198', // Tier-1
'802234830384267315', // admin
],
'discord_bot_token' => env('DISCORD_BOT_TOKEN'),
+18 -18
View File
@@ -2,42 +2,42 @@
return [
// Download Domain
'download_domain' => [
'https://imoto-ddl.ane-h.xyz',
'https://chibi-ddl.imoto-h.xyz',
'https://koneko-ddl.musume-h.xyz',
'download_domain' => [
'https://imoto-ddl.ane-h.xyz',
'https://chibi-ddl.imoto-h.xyz',
'https://koneko-ddl.musume-h.xyz',
'https://shinobu-ddl.rorikon-h.xyz',
'https://oppai-ddl.shoujo-h.org',
],
// 4k Download Domain
'download_domain_4k' => [
'https://imoto-ddlp.ane-h.xyz',
'https://chibi-ddlp.imoto-h.xyz',
'https://koneko-ddlp.musume-h.xyz',
'download_domain_4k' => [
'https://imoto-ddlp.ane-h.xyz',
'https://chibi-ddlp.imoto-h.xyz',
'https://koneko-ddlp.musume-h.xyz',
'https://shinobu-ddlp.rorikon-h.xyz',
'https://oppai-ddlp.shoujo-h.org',
],
// Stream Domain
'stream_domain' => [
'https://imoto-str.ane-h.xyz',
'https://chibi-str.imoto-h.xyz',
'https://koneko-str.musume-h.xyz',
'stream_domain' => [
'https://imoto-str.ane-h.xyz',
'https://chibi-str.imoto-h.xyz',
'https://koneko-str.musume-h.xyz',
'https://shinobu-str.rorikon-h.xyz',
'https://oppai-str.shoujo-h.org',
],
// Asia Fallback (HTTP)
'asia_download_domain' => [
'https://komako-b-ddl.musume-h.xyz'
'asia_download_domain' => [
'https://komako-b-ddl.musume-h.xyz',
],
'asia_stream_domain' => [
'https://komako-b-str.musume-h.xyz'
'asia_stream_domain' => [
'https://komako-b-str.musume-h.xyz',
],
// Free 4k Downloads
'free_downloads' => true,
'free_downloads_count' => 10,
'free_downloads' => true,
'free_downloads_count' => 10,
];
+4 -2
View File
@@ -1,5 +1,7 @@
<?php
use Intervention\Image\Drivers\Gd\Driver;
return [
/*
@@ -16,7 +18,7 @@ return [
|
*/
'driver' => \Intervention\Image\Drivers\Gd\Driver::class,
'driver' => Driver::class,
/*
|--------------------------------------------------------------------------
@@ -38,5 +40,5 @@ return [
'autoOrientation' => true,
'decodeAnimation' => true,
'blendingColor' => 'ffffff',
]
],
];
-46
View File
@@ -1,46 +0,0 @@
<?php
return [
/*
* Indicates whenever should autodetect and apply the language of the request.
*/
'autodetect' => env('LANG_DETECTOR_AUTODETECT', true),
/*
* Default driver to use to detect the request language.
*
* Available: browser, subdomain, uri.
*/
'driver' => env('LANG_DETECTOR_DRIVER', 'browser'),
/*
* Used on subdomain and uri drivers. That indicates which segment should be used
* to verify the language.
*/
'segment' => env('LANG_DETECTOR_SEGMENT', 0),
/*
* Languages available on the application.
*
* You could use parse_langs_to_array to use the string syntax
* or just use the array of languages with its aliases.
*/
'languages' => parse_langs_to_array(
env('LANG_DETECTOR_LANGUAGES', ['en', 'de', 'fr'])
),
/*
* Indicates if should store detected locale on cookies
*/
'cookie' => (bool) env('LANG_DETECTOR_COOKIE', true),
/*
* Indicates if should encrypt cookie
*/
'cookie_encrypt' => (bool) env('LANG_DETECTOR_COOKIE_ENCRYPT', false),
/*
* Cookie name
*/
'cookie_name' => env('LANG_DETECTOR_COOKIE', 'locale'),
];
+49
View File
@@ -0,0 +1,49 @@
<?php
use App\Models\User;
use Spatie\LaravelPasskeys\Actions\ConfigureCeremonyStepManagerFactoryAction;
use Spatie\LaravelPasskeys\Actions\FindPasskeyToAuthenticateAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyAuthenticationOptionsAction;
use Spatie\LaravelPasskeys\Actions\GeneratePasskeyRegisterOptionsAction;
use Spatie\LaravelPasskeys\Actions\StorePasskeyAction;
use Spatie\LaravelPasskeys\Models\Passkey;
return [
/*
* After a successful authentication attempt using a passkey
* we'll redirect to this URL.
*/
'redirect_to_after_login' => '/',
/*
* These class are responsible for performing core tasks regarding passkeys.
* You can customize them by creating a class that extends the default, and
* by specifying your custom class name here.
*/
'actions' => [
'generate_passkey_register_options' => GeneratePasskeyRegisterOptionsAction::class,
'store_passkey' => StorePasskeyAction::class,
'generate_passkey_authentication_options' => GeneratePasskeyAuthenticationOptionsAction::class,
'find_passkey' => FindPasskeyToAuthenticateAction::class,
'configure_ceremony_step_manager_factory' => ConfigureCeremonyStepManagerFactoryAction::class,
],
/*
* These properties will be used to generate the passkey.
*/
'relying_party' => [
'name' => config('app.name'),
'id' => parse_url(config('app.url'), PHP_URL_HOST),
'icon' => null,
],
/*
* The models used by the package.
*
* You can override this by specifying your own models
*/
'models' => [
'passkey' => Passkey::class,
'authenticatable' => env('AUTH_MODEL', User::class),
],
];

Some files were not shown because too many files have changed in this diff Show More