Compare commits
85 Commits
laravel-12
...
60f6c639e5
| Author | SHA1 | Date | |
|---|---|---|---|
| 60f6c639e5 | |||
| df6246e3c9 | |||
| 338c190ae8 | |||
| d08e280ce0 | |||
| 30a6b080eb | |||
| 2076517ebd | |||
| 71d679cc5e | |||
| 9dcf13a62e | |||
| 7f9573fe0f | |||
| 7553b9f895 | |||
| 5af1c3c447 | |||
| 2b1a967065 | |||
| ee1c17b903 | |||
| a5bf2ac245 | |||
| 4f81164b44 | |||
| 859a35847a | |||
| f35d1a119e | |||
| a04d58c60f | |||
| bcd15d8569 | |||
| 133a069890 | |||
| b32465a7d0 | |||
| f8022b3f18 | |||
| 57d1ec34c3 | |||
| 81639aaabf | |||
| 5dc1bff60c | |||
| 2f3f0edc30 | |||
| a71b2976af | |||
| 2c016274ab | |||
| 5ba0a55316 | |||
| a6fe34a0d1 | |||
| bb53e06c69 | |||
| 5cae5dc658 | |||
| 356d07365f | |||
| 3574d20fae | |||
| 9fc9e8ed10 | |||
| f5c706b587 | |||
| cbea71d9ae | |||
| 64a621173c | |||
| 839779b82e | |||
| 112cf9433e | |||
| 26a6500fca | |||
| d8cf70e747 | |||
| 0d4545c2ab | |||
| 900103e1c2 | |||
| d4c90976f8 | |||
| 72263127df | |||
| 6d3de59929 | |||
| ddb1bc2d14 | |||
| 5f3874a233 | |||
| ba3650899e | |||
| 904604fcfb | |||
| b7b34b503c | |||
| 4928733383 | |||
| 6340302ac6 | |||
| c1829ba7bd | |||
| 0b155bbb80 | |||
| 9f959efa14 | |||
| 38e3346dc3 | |||
| 09c08f3fea | |||
| 75f631c3e6 | |||
| fdf26604f3 | |||
| 59cb39ca77 | |||
| 62647be75c | |||
| de6efb877c | |||
| 2151d69791 | |||
| 05d4ef1bdb | |||
| 8ae9eaaadb | |||
| 361b511c3e | |||
| 1bc505057f | |||
| a78b1c41ac | |||
| 2480c5b309 | |||
| 4fc11d7329 | |||
| f3e5100d5d | |||
| 5b4d3d435e | |||
| 564f816fb9 | |||
| 3709e378c3 | |||
| 4a45dae593 | |||
| 2b0448d517 | |||
| 3bb6af73c3 | |||
| 57cf153560 | |||
| e45fd4b148 | |||
|
d479369770
|
|||
| b8ba17b33f | |||
| 5a8dd12cb8 | |||
| 3a77c4320d |
@@ -11,6 +11,7 @@
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.env.testing
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
@@ -21,3 +22,5 @@ yarn-error.log
|
||||
/.idea
|
||||
/.vscode
|
||||
.directory
|
||||
/.kilo
|
||||
/.cursor
|
||||
@@ -0,0 +1,209 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.4
|
||||
- laravel/framework (LARAVEL) - v12
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/scout (SCOUT) - v10
|
||||
- laravel/socialite (SOCIALITE) - v5
|
||||
- livewire/livewire (LIVEWIRE) - v3
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/breeze (BREEZE) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pint (PINT) - v1
|
||||
- phpunit/phpunit (PHPUNIT) - v11
|
||||
- alpinejs (ALPINEJS) - v3
|
||||
- tailwindcss (TAILWINDCSS) - v3
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
## Tools
|
||||
|
||||
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
|
||||
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
|
||||
- Use `database-schema` to inspect table structure before writing migrations or models.
|
||||
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
|
||||
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
|
||||
|
||||
## Searching Documentation (IMPORTANT)
|
||||
|
||||
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
|
||||
- Pass a `packages` array to scope results when you know which packages are relevant.
|
||||
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
|
||||
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Search Syntax
|
||||
|
||||
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
|
||||
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
|
||||
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
|
||||
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||
|
||||
## Tinker
|
||||
|
||||
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
|
||||
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
|
||||
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
|
||||
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
|
||||
- Follow existing application Enum naming conventions.
|
||||
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||
- Use array shape type definitions in PHPDoc blocks.
|
||||
|
||||
=== deployments rules ===
|
||||
|
||||
# Deployment
|
||||
|
||||
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||
|
||||
=== tests rules ===
|
||||
|
||||
# Test Enforcement
|
||||
|
||||
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
|
||||
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
## APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
|
||||
|
||||
=== laravel/v12 rules ===
|
||||
|
||||
# Laravel 12
|
||||
|
||||
- CRITICAL: ALWAYS use `search-docs` tool for version-specific Laravel documentation and updated code examples.
|
||||
- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure.
|
||||
- This is perfectly fine and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it.
|
||||
|
||||
## Laravel 10 Structure
|
||||
|
||||
- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`.
|
||||
- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure:
|
||||
- Middleware registration happens in `app/Http/Kernel.php`
|
||||
- Exception handling is in `app/Exceptions/Handler.php`
|
||||
- Console commands and schedule register in `app/Console/Kernel.php`
|
||||
- Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php`
|
||||
|
||||
## Database
|
||||
|
||||
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
|
||||
- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
|
||||
|
||||
### Models
|
||||
|
||||
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
|
||||
|
||||
=== livewire/core rules ===
|
||||
|
||||
# Livewire
|
||||
|
||||
- Livewire allow to build dynamic, reactive interfaces in PHP without writing JavaScript.
|
||||
- You can use Alpine.js for client-side interactions instead of JavaScript frameworks.
|
||||
- Keep state server-side so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.
|
||||
|
||||
=== pint/core rules ===
|
||||
|
||||
# Laravel Pint Code Formatter
|
||||
|
||||
- If you have modified any PHP files, you must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style.
|
||||
- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues.
|
||||
|
||||
=== phpunit/core rules ===
|
||||
|
||||
# PHPUnit
|
||||
|
||||
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test.
|
||||
- If you see a test using "Pest", convert it to PHPUnit.
|
||||
- Every time a test has been updated, run that singular test.
|
||||
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||
- Tests should cover all happy paths, failure paths, and edge cases.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||
- To run all tests: `php artisan test --compact`.
|
||||
- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
+116
-19
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,65 +2,51 @@
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Episode to existing series
|
||||
*/
|
||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
||||
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
|
||||
|
||||
// Create Episode
|
||||
$episode = $this->episodeService->createEpisode($request, $referenceEpisode->hentai, $episodeNumber, null, $referenceEpisode);
|
||||
$this->episodeService->createOrUpdateCover($request, $episode, $referenceEpisode->hentai->slug, 1);
|
||||
$this->downloadService->createOrUpdateDownloads($request, $episode, 1);
|
||||
$this->galleryService->createOrUpdateGallery($request, $referenceEpisode->hentai, $episode, $episodeNumber, true);
|
||||
|
||||
// Discord Alert
|
||||
if ($request->has('censored')) {
|
||||
DiscordReleaseNotification::dispatch($referenceEpisode->title." - ".$episodeNumber, 'release-censored');
|
||||
} else {
|
||||
DiscordReleaseNotification::dispatch($episode->slug, 'release');
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
|
||||
return to_route('hentai.index', [
|
||||
'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 +73,7 @@ class EpisodeController extends Controller
|
||||
cache()->flush();
|
||||
|
||||
return to_route('hentai.index', [
|
||||
'title' => $episode->slug
|
||||
'title' => $episode->slug,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,86 +3,15 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Hentai;
|
||||
|
||||
use App\Jobs\DiscordReleaseNotification;
|
||||
use App\Services\DownloadService;
|
||||
use App\Services\EpisodeService;
|
||||
use App\Services\GalleryService;
|
||||
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,
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload New Hentai with One or Multipe Episodes
|
||||
*/
|
||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
// Create new Hentai or find existing one
|
||||
$slug = $this->episodeService->generateSlug($request->input('title'));
|
||||
|
||||
$hentai = Hentai::where('slug', $slug)->first();
|
||||
|
||||
// If hentai exists and was created today, return to home
|
||||
if ($hentai?->created_at->isToday()) {
|
||||
return to_route('home.index');
|
||||
}
|
||||
|
||||
// If hentai does not exist, create a new instance
|
||||
$hentai = Hentai::firstOrCreate(
|
||||
['slug' => $slug],
|
||||
['description' => $request->input('description1')]
|
||||
);
|
||||
|
||||
// Studio
|
||||
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
||||
|
||||
// Create Episode(s)
|
||||
$releasedEpisodes = [];
|
||||
for ($i = 1; $i <= $request->input('episodes'); $i++) {
|
||||
|
||||
$episode = $this->episodeService->createEpisode($request, $hentai, $i, $studio);
|
||||
|
||||
$this->episodeService->createOrUpdateCover($request, $episode, $slug, $i);
|
||||
$this->downloadService->createOrUpdateDownloads($request, $episode, $i);
|
||||
$this->galleryService->createOrUpdateGallery($request, $hentai, $episode, $i);
|
||||
|
||||
$releasedEpisodes[] = $episode->slug;
|
||||
}
|
||||
|
||||
if ($request->has('censored')) {
|
||||
DiscordReleaseNotification::dispatch($request->input('title'), 'release-censored');
|
||||
} else {
|
||||
foreach ($releasedEpisodes as $slug) {
|
||||
// Dispatch Discord Alert
|
||||
DiscordReleaseNotification::dispatch($slug, 'release');
|
||||
}
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
|
||||
return to_route('home.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
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');
|
||||
}
|
||||
@@ -23,10 +24,9 @@ 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']);
|
||||
|
||||
@@ -40,7 +40,7 @@ class UserController extends Controller
|
||||
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,13 +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;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class DownloadApiController extends Controller
|
||||
{
|
||||
@@ -18,8 +16,8 @@ class DownloadApiController extends Controller
|
||||
public function getDownload(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'episode_id' => ['required'],
|
||||
'captcha' => ['required', new ValidAltcha],
|
||||
'episode_id' => ['required'],
|
||||
'captcha' => ['required', new ValidCaptcha],
|
||||
]);
|
||||
|
||||
$episode = Episode::where('id', $request->input('episode_id'))
|
||||
@@ -33,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,
|
||||
];
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,14 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -28,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();
|
||||
|
||||
@@ -77,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;
|
||||
}
|
||||
|
||||
@@ -90,6 +94,7 @@ class DiscordAuthController extends Controller
|
||||
// User is not in the guild
|
||||
if ($response->status() === 404) {
|
||||
$user->removeRole(UserRole::SUPPORTER);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,11 +114,12 @@ class DiscordAuthController extends Controller
|
||||
$patreonRoles = config('discord.patreon_roles', []);
|
||||
|
||||
// If intersect of array is empty, then the user doesn't have the role
|
||||
$hasSupporterRole = !empty(array_intersect($discordRoles, $patreonRoles));
|
||||
$hasSupporterRole = ! empty(array_intersect($discordRoles, $patreonRoles));
|
||||
|
||||
if (!$hasSupporterRole) {
|
||||
if (! $hasSupporterRole) {
|
||||
// Remove role if not found
|
||||
$user->removeRole(UserRole::SUPPORTER);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,21 +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 GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
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
|
||||
{
|
||||
@@ -26,7 +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 ValidAltcha],
|
||||
'altcha' => ['required', new ValidCaptcha],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Contact;
|
||||
use App\Rules\ValidCaptcha;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
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');
|
||||
}
|
||||
@@ -20,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',
|
||||
'altcha' => ['required', new ValidAltcha],
|
||||
'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');
|
||||
|
||||
@@ -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,19 +97,31 @@ 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
|
||||
{
|
||||
abort_unless(in_array($request->language, config('app.supported_locales'), true), 404);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,56 +50,40 @@ 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();
|
||||
|
||||
// Playlist
|
||||
if ($request->has('playlist')) {
|
||||
// Get and check if playlist exists
|
||||
$playlist = Playlist::where('id', $request->input('playlist'))->firstOrFail();
|
||||
$playlist = Playlist::withCount('episodes')->where('id', $request->input('playlist'))->firstOrFail();
|
||||
|
||||
// Check if episode is in playlist
|
||||
$inPlaylist = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->firstOrFail();
|
||||
|
||||
// Get Playlist Episodes and order them
|
||||
$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,
|
||||
'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
-29
@@ -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,20 +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,
|
||||
\App\Http\Middleware\SetLocale::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,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -55,18 +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,
|
||||
'auth.moderator' => \App\Http\Middleware\IsModerator::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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
<?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 Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsAdmin {
|
||||
|
||||
class IsAdmin
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if(Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR))
|
||||
{
|
||||
if (Auth::check() && Auth::user()->hasRole(UserRole::ADMINISTRATOR)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
session()->flash('error_msg','This resource is restricted to Administrators!');
|
||||
return redirect()->route('home.index');
|
||||
session()->flash('error_msg', 'This resource is restricted to Administrators!');
|
||||
|
||||
return redirect()->route('home.index');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +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 Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IsBanned {
|
||||
|
||||
class IsBanned
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if(Auth::check() && Auth::user()->hasRole(UserRole::BANNED))
|
||||
{
|
||||
if (Auth::check() && Auth::user()->hasRole(UserRole::BANNED)) {
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('home.banned');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@@ -14,16 +13,18 @@ class IsModerator
|
||||
/**
|
||||
* 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): Response
|
||||
{
|
||||
if (Auth::check() && Auth::user()->hasRole(UserRole::MODERATOR))
|
||||
{
|
||||
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 Administrators!');
|
||||
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
|
||||
{
|
||||
|
||||
@@ -13,19 +13,23 @@ class SetLocale
|
||||
/**
|
||||
* 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): 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 (session()->has('locale') && in_array($request->language, config('app.supported_locales'), true)) {
|
||||
if ($request->session()->has('locale') &&
|
||||
in_array(session('locale'), config('app.supported_locales'), true)) {
|
||||
|
||||
App::setLocale(session('locale'));
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
@@ -24,21 +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 ValidAltcha],
|
||||
'altcha' => ['required', new ValidCaptcha],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
@@ -58,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,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',
|
||||
|
||||
@@ -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,30 @@ class DiscordReleaseNotification implements ShouldQueue
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
switch($this->messageType)
|
||||
{
|
||||
// Discord notifications must only ever be sent in production.
|
||||
if (! app()->isProduction()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Jobs\DiscordReleaseNotification;
|
||||
use App\Models\Episode;
|
||||
use App\Services\CdnPathValidator;
|
||||
use App\Services\DownloadService;
|
||||
use App\Services\EpisodeService;
|
||||
use App\Services\GalleryService;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class AdminEpisodeForm extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public int $referenceEpisodeId;
|
||||
|
||||
public int $episodeNumber;
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $titleJpn = '';
|
||||
|
||||
public string $studio = '';
|
||||
|
||||
/** @var string[] */
|
||||
public array $tags = [];
|
||||
|
||||
public string $releasedate = '';
|
||||
|
||||
public string $baseurl = '';
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public ?TemporaryUploadedFile $cover = null;
|
||||
|
||||
/** @var TemporaryUploadedFile[] */
|
||||
public array $gallery = [];
|
||||
|
||||
/** @var array{fhd: string, fhdi: string, uhd: string, uhdi: string} */
|
||||
public array $downloads = ['fhd' => '', 'fhdi' => '', 'uhd' => '', 'uhdi' => ''];
|
||||
|
||||
public bool $censored = false;
|
||||
|
||||
/** @var array<string, array{state: string, message: string, mirrors: array}> */
|
||||
public array $validation = [];
|
||||
|
||||
public bool $overrideValidation = false;
|
||||
|
||||
public bool $saving = false;
|
||||
|
||||
public function mount(int $episodeId): void
|
||||
{
|
||||
abort_unless(auth()->user()?->hasRole(UserRole::ADMINISTRATOR), 403);
|
||||
|
||||
$reference = Episode::with(['hentai', 'studio'])->findOrFail($episodeId);
|
||||
|
||||
$this->referenceEpisodeId = $reference->id;
|
||||
$this->episodeNumber = $reference->hentai->episodes()->count() + 1;
|
||||
|
||||
$this->title = $reference->title;
|
||||
$this->titleJpn = $reference->title_jpn;
|
||||
$this->studio = $reference->studio?->name ?? '';
|
||||
$this->tags = $reference->tags->pluck('name')->all();
|
||||
$this->releasedate = $reference->release_date
|
||||
? Carbon::parse($reference->release_date)->format('Y-m-d')
|
||||
: now()->format('Y-m-d');
|
||||
|
||||
$this->baseurl = preg_replace('#/E\d+$#', '', $reference->url) ?? '';
|
||||
$this->description = $reference->description;
|
||||
}
|
||||
|
||||
public function getHasInvalidValidationProperty(): bool
|
||||
{
|
||||
foreach ($this->validation as $status) {
|
||||
if (($status['state'] ?? '') === 'invalid') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updatedBaseurl(): void
|
||||
{
|
||||
$this->validateStream();
|
||||
}
|
||||
|
||||
public function updatedDownloads(mixed $value, string $key): void
|
||||
{
|
||||
if (preg_match('/^(fhd|fhdi|uhd|uhdi)$/', $key)) {
|
||||
$this->validateDownload($key);
|
||||
}
|
||||
}
|
||||
|
||||
public function validateStream(): void
|
||||
{
|
||||
$baseurl = trim($this->baseurl);
|
||||
|
||||
if ($baseurl === '') {
|
||||
$this->validation['stream'] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation['stream'] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateStream($baseurl, 1, false, $this->episodeNumber);
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateDownload(string $quality): void
|
||||
{
|
||||
$key = "downloads.{$quality}";
|
||||
$url = $this->downloads[$quality] ?? '';
|
||||
|
||||
if (trim($url) === '') {
|
||||
$this->validation[$key] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation[$key] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateDownload($this->qualityToType($quality), $url);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
abort_unless(auth()->user()?->hasRole(UserRole::ADMINISTRATOR), 403);
|
||||
|
||||
$this->validate([
|
||||
'baseurl' => ['required', 'string', 'regex:/^[A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+/'],
|
||||
'description' => 'required|string',
|
||||
'cover' => 'required|image|max:20480',
|
||||
'gallery.*' => 'nullable|image|max:20480',
|
||||
'downloads.fhd' => 'required|string',
|
||||
'downloads.uhd' => 'required|string',
|
||||
'downloads.fhdi' => 'nullable|string',
|
||||
'downloads.uhdi' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$this->saving = true;
|
||||
|
||||
try {
|
||||
// Re-load the reference episode fresh so the episode number reflects
|
||||
// the current episode count rather than the mount-time value.
|
||||
$referenceEpisode = Episode::with(['hentai', 'studio'])->findOrFail($this->referenceEpisodeId);
|
||||
$hentai = $referenceEpisode->hentai;
|
||||
$this->episodeNumber = $hentai->episodes()->count() + 1;
|
||||
|
||||
// Re-validate every non-empty download path + stream against the CDN.
|
||||
$validator = app(CdnPathValidator::class);
|
||||
$sizes = [];
|
||||
|
||||
foreach ($this->downloads as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$key = "downloads.{$quality}";
|
||||
$result = $validator->validateDownload($type, $url, true);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
$sizes[$type] = $result['size'];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError($key, 'Path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $validator->validateStream($this->baseurl, 1, true, $this->episodeNumber);
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError('baseurl', 'Stream path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist.
|
||||
$episodeService = app(EpisodeService::class);
|
||||
$studio = $episodeService->getOrCreateStudio(trim($this->studio));
|
||||
|
||||
$episodeModel = $episodeService->createEpisodeFromArray([
|
||||
'title' => $this->title,
|
||||
'title_jpn' => $this->titleJpn,
|
||||
'baseurl' => $this->baseurl,
|
||||
'description' => $this->description,
|
||||
'releasedate' => $this->releasedate,
|
||||
'tags' => $this->tags,
|
||||
'interpolated_uhd' => trim($this->downloads['uhdi'] ?? '') !== '',
|
||||
], $hentai, $this->episodeNumber, $studio);
|
||||
|
||||
if ($this->cover) {
|
||||
$episodeService->saveCoverFromFile($episodeModel, $hentai->slug, $this->episodeNumber, $this->cover);
|
||||
}
|
||||
|
||||
if (! empty($this->gallery)) {
|
||||
app(GalleryService::class)->saveGalleryFiles($hentai, $episodeModel, $this->episodeNumber, $this->gallery);
|
||||
}
|
||||
|
||||
$downloads = [];
|
||||
foreach ($this->downloads as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$downloads[$type] = [
|
||||
'url' => trim($url),
|
||||
'size' => $sizes[$type] ?? null,
|
||||
];
|
||||
}
|
||||
app(DownloadService::class)->createOrUpdateDownloadsFromArray($episodeModel, $downloads);
|
||||
|
||||
// Discord notifications + cache flush.
|
||||
if ($this->censored) {
|
||||
DiscordReleaseNotification::dispatch($this->title.' - '.$this->episodeNumber, 'release-censored');
|
||||
} else {
|
||||
DiscordReleaseNotification::dispatch($episodeModel->slug, 'release');
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to create episode from Livewire form: '.$e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'referenceEpisodeId' => $this->referenceEpisodeId,
|
||||
]);
|
||||
$this->saving = false;
|
||||
$this->addError('description', 'Something went wrong while saving. Please try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->redirectRoute('hentai.index', ['title' => $episodeModel->slug]);
|
||||
}
|
||||
|
||||
private function qualityToType(string $quality): string
|
||||
{
|
||||
return match ($quality) {
|
||||
'fhd' => 'FHD',
|
||||
'fhdi' => 'FHDi',
|
||||
'uhd' => 'UHD',
|
||||
'uhdi' => 'UHDi',
|
||||
};
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin-episode-form');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Jobs\DiscordReleaseNotification;
|
||||
use App\Models\Hentai;
|
||||
use App\Services\CdnPathValidator;
|
||||
use App\Services\DownloadService;
|
||||
use App\Services\EpisodeService;
|
||||
use App\Services\GalleryService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
use Livewire\WithFileUploads;
|
||||
|
||||
class AdminReleaseForm extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $titleJpn = '';
|
||||
|
||||
public string $studio = '';
|
||||
|
||||
/** @var string[] */
|
||||
public array $tags = [];
|
||||
|
||||
public string $releasedate = '';
|
||||
|
||||
public string $baseurl = '';
|
||||
|
||||
public bool $censored = false;
|
||||
|
||||
public int $episodeCount = 1;
|
||||
|
||||
/** @var array<int, array{description: string, cover: TemporaryUploadedFile|null, gallery: TemporaryUploadedFile[], downloads: array{fhd: string, fhdi: string, uhd: string, uhdi: string}}> */
|
||||
public array $episodes = [];
|
||||
|
||||
/** @var array<string, array{state: string, message: string, mirrors: array}> */
|
||||
public array $validation = [];
|
||||
|
||||
public bool $overrideValidation = false;
|
||||
|
||||
public bool $saving = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->episodes = [$this->defaultEpisode()];
|
||||
}
|
||||
|
||||
private function defaultEpisode(): array
|
||||
{
|
||||
return [
|
||||
'description' => '',
|
||||
'cover' => null,
|
||||
'gallery' => [],
|
||||
'downloads' => ['fhd' => '', 'fhdi' => '', 'uhd' => '', 'uhdi' => ''],
|
||||
];
|
||||
}
|
||||
|
||||
public function addEpisode(): void
|
||||
{
|
||||
$this->episodes[] = $this->defaultEpisode();
|
||||
$this->episodeCount = count($this->episodes);
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$this->validateStream();
|
||||
}
|
||||
}
|
||||
|
||||
public function removeEpisode(int $index): void
|
||||
{
|
||||
if (count($this->episodes) <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($this->episodes[$index]);
|
||||
$this->episodes = array_values($this->episodes);
|
||||
$this->episodeCount = count($this->episodes);
|
||||
$this->pruneValidation();
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$this->validateStream();
|
||||
}
|
||||
}
|
||||
|
||||
public function getHasInvalidValidationProperty(): bool
|
||||
{
|
||||
foreach ($this->validation as $status) {
|
||||
if (($status['state'] ?? '') === 'invalid') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updatedEpisodes(mixed $value, string $key): void
|
||||
{
|
||||
if (preg_match('/^(\d+)\.downloads\.(fhd|fhdi|uhd|uhdi)$/', $key, $matches)) {
|
||||
$this->validateDownload((int) $matches[1], $matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedBaseurl(): void
|
||||
{
|
||||
$this->validateStream();
|
||||
}
|
||||
|
||||
public function validateDownload(int $episodeIndex, string $quality): void
|
||||
{
|
||||
$key = "episodes.{$episodeIndex}.downloads.{$quality}";
|
||||
$url = $this->episodes[$episodeIndex]['downloads'][$quality] ?? '';
|
||||
|
||||
if (trim($url) === '') {
|
||||
$this->validation[$key] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation[$key] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateDownload($this->qualityToType($quality), $url);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validateStream(): void
|
||||
{
|
||||
$baseurl = trim($this->baseurl);
|
||||
|
||||
if ($baseurl === '') {
|
||||
$this->validation['stream'] = ['state' => 'idle', 'message' => '', 'mirrors' => []];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validation['stream'] = ['state' => 'checking', 'message' => 'Checking mirrors…', 'mirrors' => []];
|
||||
|
||||
$result = app(CdnPathValidator::class)->validateStream($baseurl, count($this->episodes));
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'titleJpn' => 'required|string|max:255',
|
||||
'studio' => 'required|string|max:255',
|
||||
'tags' => 'required|array|min:1',
|
||||
'releasedate' => 'required|date',
|
||||
'baseurl' => ['required', 'string', 'regex:/^[A-Za-z0-9_.\-]+\/[A-Za-z0-9_.\-]+/'],
|
||||
'episodes.*.description' => 'required|string',
|
||||
'episodes.*.cover' => 'required|image|max:20480',
|
||||
'episodes.*.gallery.*' => 'nullable|image|max:20480',
|
||||
'episodes.*.downloads.fhd' => 'required|string',
|
||||
'episodes.*.downloads.uhd' => 'required|string',
|
||||
]);
|
||||
|
||||
$this->saving = true;
|
||||
|
||||
// 1) Re-validate every non-empty download path + stream against the CDN.
|
||||
$validator = app(CdnPathValidator::class);
|
||||
$sizes = [];
|
||||
|
||||
foreach ($this->episodes as $index => $episode) {
|
||||
foreach ($episode['downloads'] as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$key = "episodes.{$index}.downloads.{$quality}";
|
||||
$result = $validator->validateDownload($type, $url, true);
|
||||
|
||||
$this->validation[$key] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
$sizes[$index][$type] = $result['size'];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError($key, 'Path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trim($this->baseurl) !== '') {
|
||||
$result = $validator->validateStream($this->baseurl, count($this->episodes), true);
|
||||
|
||||
$this->validation['stream'] = [
|
||||
'state' => $result['valid'] ? 'valid' : 'invalid',
|
||||
'message' => $result['valid'] ? 'Verified on all mirrors' : 'Missing on one or more mirrors',
|
||||
'mirrors' => $result['mirrors'],
|
||||
];
|
||||
|
||||
if (! $result['valid'] && ! $this->overrideValidation) {
|
||||
$this->addError('baseurl', 'Stream path not found on all CDN mirrors.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Duplicate-guard: block re-releasing a title created today.
|
||||
$episodeService = app(EpisodeService::class);
|
||||
$slug = $episodeService->generateSlug($this->title);
|
||||
|
||||
$existing = Hentai::where('slug', $slug)->first();
|
||||
if ($existing && $existing->created_at->isToday()) {
|
||||
$this->addError('title', 'A release with this title was already created today. Refine the title to avoid a duplicate.');
|
||||
$this->saving = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 3) Persist.
|
||||
$hentai = Hentai::firstOrCreate(
|
||||
['slug' => $slug],
|
||||
['description' => $this->episodes[0]['description']]
|
||||
);
|
||||
|
||||
$studio = $episodeService->getOrCreateStudio(trim($this->studio));
|
||||
|
||||
$galleryService = app(GalleryService::class);
|
||||
$downloadService = app(DownloadService::class);
|
||||
|
||||
$releasedEpisodes = [];
|
||||
foreach ($this->episodes as $index => $episode) {
|
||||
$episodeNumber = $index + 1;
|
||||
|
||||
$episodeModel = $episodeService->createEpisodeFromArray([
|
||||
'title' => $this->title,
|
||||
'title_jpn' => $this->titleJpn,
|
||||
'baseurl' => $this->baseurl,
|
||||
'description' => $episode['description'],
|
||||
'releasedate' => $this->releasedate,
|
||||
'tags' => $this->tags,
|
||||
'interpolated_uhd' => trim($episode['downloads']['uhdi'] ?? '') !== '',
|
||||
], $hentai, $episodeNumber, $studio);
|
||||
|
||||
if ($episode['cover']) {
|
||||
$episodeService->saveCoverFromFile($episodeModel, $hentai->slug, $episodeNumber, $episode['cover']);
|
||||
}
|
||||
|
||||
if (! empty($episode['gallery'])) {
|
||||
$galleryService->saveGalleryFiles($hentai, $episodeModel, $episodeNumber, $episode['gallery']);
|
||||
}
|
||||
|
||||
$downloads = [];
|
||||
foreach ($episode['downloads'] as $quality => $url) {
|
||||
if (trim($url) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->qualityToType($quality);
|
||||
$downloads[$type] = [
|
||||
'url' => trim($url),
|
||||
'size' => $sizes[$index][$type] ?? null,
|
||||
];
|
||||
}
|
||||
$downloadService->createOrUpdateDownloadsFromArray($episodeModel, $downloads);
|
||||
|
||||
$releasedEpisodes[] = $episodeModel->slug;
|
||||
}
|
||||
|
||||
// 4) Discord notifications + cache flush.
|
||||
if ($this->censored) {
|
||||
DiscordReleaseNotification::dispatch($this->title, 'release-censored');
|
||||
} else {
|
||||
foreach ($releasedEpisodes as $episodeSlug) {
|
||||
DiscordReleaseNotification::dispatch($episodeSlug, 'release');
|
||||
}
|
||||
}
|
||||
|
||||
cache()->flush();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Failed to create release from Livewire form: '.$e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
$this->saving = false;
|
||||
$this->addError('title', 'Something went wrong while saving. Please try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 5) Redirect.
|
||||
$this->redirectRoute('home.index');
|
||||
}
|
||||
|
||||
private function qualityToType(string $quality): string
|
||||
{
|
||||
return match ($quality) {
|
||||
'fhd' => 'FHD',
|
||||
'fhdi' => 'FHDi',
|
||||
'uhd' => 'UHD',
|
||||
'uhdi' => 'UHDi',
|
||||
};
|
||||
}
|
||||
|
||||
private function pruneValidation(): void
|
||||
{
|
||||
$count = count($this->episodes);
|
||||
foreach (array_keys($this->validation) as $key) {
|
||||
if (preg_match('/^episodes\.(\d+)\./', $key, $matches) && (int) $matches[1] >= $count) {
|
||||
unset($this->validation[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin-release-form');
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,9 @@ 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;
|
||||
|
||||
class AdminUserSearch extends Component
|
||||
{
|
||||
@@ -21,32 +20,239 @@ class AdminUserSearch extends Component
|
||||
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();
|
||||
}
|
||||
|
||||
Comment::where('user_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->patreon !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::SUPPORTER->value))
|
||||
->when($this->banned !== [], fn ($query) => $query->whereJsonContains('roles', UserRole::BANNED->value))
|
||||
->when($this->search !== '', fn ($query) => $query->where('name', 'like', '%'.$this->search.'%'))
|
||||
->when($this->discordId !== '', fn ($query) => $query->where('discord_id', '=', $this->discordId))
|
||||
->paginate(20);
|
||||
|
||||
return view('livewire.admin-user-search', [
|
||||
'users' => $users
|
||||
'users' => $this->users,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||
$types[] = 'UHD';
|
||||
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
} 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()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistEpisode;
|
||||
use App\Services\PlaylistService;
|
||||
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
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
|
||||
{
|
||||
@@ -20,13 +19,16 @@ class PlaylistOverview extends Component
|
||||
protected PlaylistService $playlistService;
|
||||
|
||||
#[Url(history: true)]
|
||||
public $search;
|
||||
public string $search = '';
|
||||
|
||||
public int $pagination = 25;
|
||||
#[Url(history: true)]
|
||||
public int $perPage = 25;
|
||||
|
||||
public Playlist $playlist;
|
||||
|
||||
public Collection $playlistEpisodes;
|
||||
public bool $editingName = false;
|
||||
|
||||
public string $editingPlaylistName = '';
|
||||
|
||||
public function boot(PlaylistService $playlistService)
|
||||
{
|
||||
@@ -35,35 +37,56 @@ class PlaylistOverview extends Component
|
||||
|
||||
public function mount($playlist_id)
|
||||
{
|
||||
$this->playlist = Playlist::with(['episodes.episode'])->findOrFail($playlist_id);
|
||||
$this->playlist = Playlist::withCount('episodes')->with('user')->findOrFail($playlist_id);
|
||||
|
||||
// Set position if null
|
||||
$this->playlist->episodes->each(function ($item, $index) {
|
||||
if ($item->position === null) {
|
||||
$item->position = $index + 1;
|
||||
$item->save();
|
||||
}
|
||||
});
|
||||
$this->sanitizePerPage();
|
||||
|
||||
$this->refreshEpisodes();
|
||||
$this->repairNullPositions();
|
||||
}
|
||||
|
||||
public function refreshEpisodes()
|
||||
public function updatingSearch(): void
|
||||
{
|
||||
$this->playlistEpisodes = $this->playlist->episodes()->orderBy('position')->with('episode')->get();
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function updatingPerPage(): void
|
||||
{
|
||||
$this->resetPage();
|
||||
}
|
||||
|
||||
public function getEpisodesProperty(): LengthAwarePaginator
|
||||
{
|
||||
$this->sanitizePerPage();
|
||||
|
||||
return PlaylistEpisode::query()
|
||||
->where('playlist_id', $this->playlist->id)
|
||||
->when($this->search !== '', fn ($query) => $query->whereHas('episode', fn ($episode) => $episode
|
||||
->where('title', 'like', '%'.$this->search.'%')
|
||||
->orWhere('title_jpn', 'like', '%'.$this->search.'%')))
|
||||
->orderBy('position')
|
||||
->with(['episode.gallery' => fn ($gallery) => $gallery->orderBy('id')->limit(1)])
|
||||
->paginate($this->perPage);
|
||||
}
|
||||
|
||||
public function getFirstEpisodeProperty(): ?Episode
|
||||
{
|
||||
return PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->orderBy('position')
|
||||
->first()?->episode;
|
||||
}
|
||||
|
||||
public function moveUp($episodeId)
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$episode = PlaylistEpisode::find($episodeId);
|
||||
|
||||
if (! $episode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$above = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
||||
->where('position', '<', $episode->position)
|
||||
->orderBy('position', 'desc')
|
||||
@@ -72,21 +95,20 @@ class PlaylistOverview extends Component
|
||||
if ($above) {
|
||||
$this->playlistService->swapPositions($episode, $above);
|
||||
}
|
||||
|
||||
$this->refreshEpisodes();
|
||||
}
|
||||
|
||||
public function moveDown($episodeId)
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$episode = PlaylistEpisode::find($episodeId);
|
||||
|
||||
if (! $episode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$below = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
||||
->where('position', '>', $episode->position)
|
||||
->orderBy('position')
|
||||
@@ -95,29 +117,102 @@ class PlaylistOverview extends Component
|
||||
if ($below) {
|
||||
$this->playlistService->swapPositions($episode, $below);
|
||||
}
|
||||
|
||||
$this->refreshEpisodes();
|
||||
}
|
||||
|
||||
public function remove($episodeId)
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistEpisode::find($episodeId)?->delete();
|
||||
$this->playlistService->reorderPositions($this->playlist);
|
||||
$this->refreshEpisodes();
|
||||
$this->playlist->loadCount('episodes');
|
||||
|
||||
$lastPage = max(1, (int) ceil(PlaylistEpisode::where('playlist_id', $this->playlist->id)->count() / $this->perPage));
|
||||
|
||||
if ($this->getPage() > $lastPage) {
|
||||
$this->setPage($lastPage);
|
||||
}
|
||||
}
|
||||
|
||||
public function editName()
|
||||
{
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->editingPlaylistName = $this->playlist->name;
|
||||
$this->editingName = true;
|
||||
}
|
||||
|
||||
public function cancelEditName()
|
||||
{
|
||||
$this->editingName = false;
|
||||
$this->editingPlaylistName = '';
|
||||
}
|
||||
|
||||
public function updateName()
|
||||
{
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->validate([
|
||||
'editingPlaylistName' => 'required|max:30',
|
||||
]);
|
||||
|
||||
$this->playlist->update([
|
||||
'name' => $this->editingPlaylistName,
|
||||
]);
|
||||
|
||||
$this->editingName = false;
|
||||
$this->editingPlaylistName = '';
|
||||
}
|
||||
|
||||
public function toggleVisibility()
|
||||
{
|
||||
if (! $this->isOwner()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->playlist->update([
|
||||
'is_private' => ! $this->playlist->is_private,
|
||||
]);
|
||||
|
||||
$this->playlist->loadCount('episodes');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.playlist-overview', [
|
||||
'query' => $this->search,
|
||||
'episodes' => $this->episodes,
|
||||
'firstEpisode' => $this->firstEpisode,
|
||||
'isOwner' => $this->isOwner(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function isOwner(): bool
|
||||
{
|
||||
return Auth::check() && Auth::user()->id === $this->playlist->user_id;
|
||||
}
|
||||
|
||||
private function sanitizePerPage(): void
|
||||
{
|
||||
if (! in_array($this->perPage, [25, 50, 100], true)) {
|
||||
$this->perPage = 25;
|
||||
}
|
||||
}
|
||||
|
||||
private function repairNullPositions(): void
|
||||
{
|
||||
if (! PlaylistEpisode::where('playlist_id', $this->playlist->id)->whereNull('position')->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->orderBy('position')->orderBy('id')
|
||||
->get()
|
||||
->each(fn ($playlistEpisode, $index) => $playlistEpisode->update(['position' => $index + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistEpisode;
|
||||
use App\Services\PlaylistService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class PlaylistSidebar extends Component
|
||||
{
|
||||
public const PAGE_SIZE = 20;
|
||||
|
||||
public const MAX_WINDOW = 60;
|
||||
|
||||
protected PlaylistService $playlistService;
|
||||
|
||||
public Playlist $playlist;
|
||||
|
||||
public Episode $currentEpisode;
|
||||
|
||||
public int $currentEpisodeId;
|
||||
|
||||
public int $total = 0;
|
||||
|
||||
public int $windowStart = 1;
|
||||
|
||||
public int $windowEnd = 0;
|
||||
|
||||
public bool $isOwner = false;
|
||||
|
||||
public bool $collapsible = false;
|
||||
|
||||
public ?string $previousEpisodeSlug = null;
|
||||
|
||||
public ?string $nextEpisodeSlug = null;
|
||||
|
||||
public function boot(PlaylistService $playlistService): void
|
||||
{
|
||||
$this->playlistService = $playlistService;
|
||||
}
|
||||
|
||||
public function mount(int $playlistId, int $currentEpisodeId, bool $collapsible = false): void
|
||||
{
|
||||
$this->playlist = Playlist::with('user')->withCount('episodes')->findOrFail($playlistId);
|
||||
$this->currentEpisodeId = $currentEpisodeId;
|
||||
$this->currentEpisode = Episode::with(['gallery' => fn ($query) => $query->orderBy('id')->limit(1)])
|
||||
->findOrFail($currentEpisodeId);
|
||||
$this->total = $this->playlist->episodes_count;
|
||||
$this->collapsible = $collapsible;
|
||||
$this->isOwner = Auth::check() && Auth::user()->id === $this->playlist->user_id;
|
||||
|
||||
$this->repairNullPositions();
|
||||
|
||||
$position = $this->currentPosition() ?? 1;
|
||||
|
||||
$this->setWindowAround($position);
|
||||
$this->resolveAdjacentSlugs($position);
|
||||
}
|
||||
|
||||
public function getEpisodesProperty(): Collection
|
||||
{
|
||||
return PlaylistEpisode::query()
|
||||
->where('playlist_id', $this->playlist->id)
|
||||
->whereBetween('position', [$this->windowStart, $this->windowEnd])
|
||||
->orderBy('position')
|
||||
->with([
|
||||
'episode.gallery' => fn ($gallery) => $gallery->orderBy('id')->limit(1),
|
||||
'episode.studio',
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
public function appendChunk(): void
|
||||
{
|
||||
if ($this->windowEnd >= $this->total) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->windowEnd = min($this->total, $this->windowEnd + self::PAGE_SIZE);
|
||||
$this->windowStart = max(1, $this->windowEnd - self::MAX_WINDOW + 1);
|
||||
}
|
||||
|
||||
public function prependChunk(): void
|
||||
{
|
||||
if ($this->windowStart <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->windowStart = max(1, $this->windowStart - self::PAGE_SIZE);
|
||||
$this->windowEnd = min($this->total, $this->windowStart + self::MAX_WINDOW - 1);
|
||||
}
|
||||
|
||||
public function remove(int $playlistEpisodeId): void
|
||||
{
|
||||
if (! $this->isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
$playlistEpisode = PlaylistEpisode::find($playlistEpisodeId);
|
||||
|
||||
if (! $playlistEpisode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$playlistEpisode->delete();
|
||||
$this->playlistService->reorderPositions($this->playlist);
|
||||
$this->playlist->loadCount('episodes');
|
||||
|
||||
$this->total = $this->playlist->episodes_count;
|
||||
|
||||
$this->windowEnd = min($this->windowEnd, max(1, $this->total));
|
||||
$this->windowStart = min($this->windowStart, max(1, $this->total));
|
||||
|
||||
$position = $this->currentPosition();
|
||||
|
||||
if ($position) {
|
||||
$this->resolveAdjacentSlugs($position);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$activePlaylistEpisode = $this->activePlaylistEpisode();
|
||||
|
||||
return view('livewire.playlist-sidebar', [
|
||||
'episodes' => $this->episodes,
|
||||
'currentPosition' => $activePlaylistEpisode?->position ?? 1,
|
||||
'currentPlaylistEpisodeId' => $activePlaylistEpisode?->id,
|
||||
'isOwner' => $this->isOwner,
|
||||
]);
|
||||
}
|
||||
|
||||
private function activePlaylistEpisode(): ?PlaylistEpisode
|
||||
{
|
||||
return PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->where('episode_id', $this->currentEpisodeId)
|
||||
->first();
|
||||
}
|
||||
|
||||
private function currentPosition(): ?int
|
||||
{
|
||||
return $this->activePlaylistEpisode()?->position;
|
||||
}
|
||||
|
||||
private function setWindowAround(int $position): void
|
||||
{
|
||||
$this->windowStart = max(1, $position - self::PAGE_SIZE);
|
||||
$this->windowEnd = min($this->total, $position + self::PAGE_SIZE);
|
||||
}
|
||||
|
||||
private function resolveAdjacentSlugs(int $position): void
|
||||
{
|
||||
$adjacent = PlaylistEpisode::query()
|
||||
->where('playlist_id', $this->playlist->id)
|
||||
->whereBetween('position', [$position - 1, $position + 1])
|
||||
->orderBy('position')
|
||||
->with('episode')
|
||||
->get();
|
||||
|
||||
$slugsByPosition = $adjacent->keyBy('position');
|
||||
|
||||
$this->previousEpisodeSlug = $slugsByPosition->get($position - 1)?->episode->slug;
|
||||
$this->nextEpisodeSlug = $slugsByPosition->get($position + 1)?->episode->slug;
|
||||
}
|
||||
|
||||
private function repairNullPositions(): void
|
||||
{
|
||||
if (! PlaylistEpisode::where('playlist_id', $this->playlist->id)->whereNull('position')->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->orderBy('position')->orderBy('id')
|
||||
->get()
|
||||
->each(fn ($playlistEpisode, $index) => $playlistEpisode->update(['position' => $index + 1]));
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Playlist;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
use Livewire\Attributes\Url;
|
||||
|
||||
class Playlists extends Component
|
||||
{
|
||||
@@ -54,12 +54,31 @@ 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.'%'))
|
||||
->with([
|
||||
'user',
|
||||
'episodes' => fn ($query) => $query->orderBy('position')->limit(4),
|
||||
'episodes.episode.gallery' => fn ($query) => $query->limit(1),
|
||||
])
|
||||
->orderBy($orderby, $orderdirection)
|
||||
->paginate($this->pagination);
|
||||
|
||||
$stats = Cache::remember('publicPlaylistStats', 600, function () {
|
||||
$playlists = Playlist::where('is_private', 0)
|
||||
->withCount('episodes')
|
||||
->having('episodes_count', '>', 1)
|
||||
->get();
|
||||
|
||||
return [
|
||||
'playlists' => $playlists->count(),
|
||||
'episodes' => $playlists->sum('episodes_count'),
|
||||
];
|
||||
});
|
||||
|
||||
return view('livewire.playlists', [
|
||||
'playlists' => $playlists
|
||||
'playlists' => $playlists,
|
||||
'totalPlaylists' => $stats['playlists'],
|
||||
'totalEpisodes' => $stats['episodes'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ class Downloads extends Model
|
||||
'episode_id',
|
||||
'type',
|
||||
'url',
|
||||
'size',
|
||||
'validated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -39,7 +41,7 @@ class Downloads extends Model
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$i];
|
||||
return round($bytes, 2).' '.$units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+30
-35
@@ -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));
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Gallery extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $table = 'gallery';
|
||||
|
||||
/**
|
||||
* The attributes that aren't mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* Belongs To Episode.
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
+24
-1
@@ -2,11 +2,34 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Playlist extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PlaylistEpisode extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Indicates If The Model Should Be Timestamped.
|
||||
*
|
||||
|
||||
@@ -13,7 +13,7 @@ class PopularDaily extends Model
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [ 'episode_id' ];
|
||||
protected $fillable = ['episode_id'];
|
||||
|
||||
/**
|
||||
* Get the Episode.
|
||||
|
||||
@@ -13,7 +13,7 @@ class PopularMonthly extends Model
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [ 'episode_id' ];
|
||||
protected $fillable = ['episode_id'];
|
||||
|
||||
/**
|
||||
* Get the Episode.
|
||||
|
||||
@@ -13,7 +13,7 @@ class PopularWeekly extends Model
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $fillable = [ 'episode_id' ];
|
||||
protected $fillable = ['episode_id'];
|
||||
|
||||
/**
|
||||
* Get the Episode.
|
||||
|
||||
@@ -25,4 +25,4 @@ class CommentPresenter
|
||||
{
|
||||
return $this->comment->created_at->diffForHumans();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
+16
-14
@@ -2,20 +2,21 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
//use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
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.
|
||||
@@ -62,7 +63,6 @@ class User extends Authenticatable
|
||||
'discord_avatar' => 'string',
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Has Many Playlists.
|
||||
*/
|
||||
@@ -100,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());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,13 +108,11 @@ 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);
|
||||
}
|
||||
|
||||
@@ -153,11 +151,15 @@ class User extends Authenticatable
|
||||
*/
|
||||
public function removeRole(UserRole $role): void
|
||||
{
|
||||
if (!$this->hasRole($role)) {
|
||||
if (! $this->hasRole($role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->roles = array_diff($this->roles, [$role->value]);
|
||||
$this->roles = collect($this->roles)
|
||||
->reject(fn ($value) => $value === $role->value)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CdnPathValidator
|
||||
{
|
||||
public const DOWNLOAD_PREFIX = [
|
||||
'FHD' => 'hentai-1080p/',
|
||||
'FHDi' => 'hentai-1080p/',
|
||||
'UHD' => 'hentai/',
|
||||
'UHDi' => 'hentai/',
|
||||
];
|
||||
|
||||
private const STREAM_PREFIX = 'hentai-stream/';
|
||||
|
||||
private const CACHE_TTL = 300;
|
||||
|
||||
public function validateDownload(string $type, string $url, bool $fresh = false): array
|
||||
{
|
||||
$path = self::DOWNLOAD_PREFIX[$type].ltrim($url, '/');
|
||||
$domains = config('hstream.download_domain_4k');
|
||||
|
||||
return $this->checkMirrors($domains, [$path], $fresh);
|
||||
}
|
||||
|
||||
public function validateStream(string $baseurl, int $episodeCount, bool $fresh = false, int $episodeNumber = 1): array
|
||||
{
|
||||
$domains = config('hstream.download_domain_4k');
|
||||
|
||||
// Validate the base directory, plus the episode manifest as a sanity check.
|
||||
$paths = [self::STREAM_PREFIX.rtrim($baseurl, '/')];
|
||||
if ($episodeCount >= 1) {
|
||||
$paths[] = self::STREAM_PREFIX.rtrim($baseurl, '/').'/E'.str_pad($episodeNumber, 2, '0', STR_PAD_LEFT).'/720/manifest.mpd';
|
||||
}
|
||||
|
||||
return $this->checkMirrors($domains, $paths, $fresh);
|
||||
}
|
||||
|
||||
private function checkMirrors(array $domains, array $paths, bool $fresh = false): array
|
||||
{
|
||||
$cacheKey = 'cdncheck:'.sha1(serialize([$domains, $paths]));
|
||||
|
||||
if ($fresh) {
|
||||
Cache::forget($cacheKey);
|
||||
}
|
||||
|
||||
return Cache::remember($cacheKey, self::CACHE_TTL, fn () => $this->runChecks($domains, $paths));
|
||||
}
|
||||
|
||||
private function runChecks(array $domains, array $paths): array
|
||||
{
|
||||
$entries = [];
|
||||
foreach ($domains as $domain) {
|
||||
foreach ($paths as $path) {
|
||||
$entries[] = ['domain' => $domain, 'path' => $path];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$responses = Http::pool(function ($pool) use ($entries) {
|
||||
foreach ($entries as $entry) {
|
||||
$pool->timeout(4)
|
||||
->withHeaders(['Accept' => 'application/json'])
|
||||
->get($entry['domain'].'/check/'.$this->tokenize($entry['path']).'/'.$this->tokenize(now()->addHours(6)));
|
||||
}
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$mirrors = [];
|
||||
foreach ($entries as $entry) {
|
||||
$mirrors[$entry['domain']][$entry['path']] = false;
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => false,
|
||||
'mirrors' => $mirrors,
|
||||
'size' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$mirrors = [];
|
||||
$allValid = true;
|
||||
$firstSize = null;
|
||||
|
||||
foreach ($entries as $index => $entry) {
|
||||
$response = $responses[$index];
|
||||
$ok = $response instanceof Response
|
||||
&& $response->ok()
|
||||
&& $response->json('valid') === true;
|
||||
|
||||
$mirrors[$entry['domain']][$entry['path']] = $ok;
|
||||
|
||||
if (! $ok) {
|
||||
$allValid = false;
|
||||
} elseif ($firstSize === null) {
|
||||
$firstSize = $response->json('size');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => $allValid,
|
||||
'mirrors' => $mirrors,
|
||||
'size' => $allValid ? $firstSize : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function tokenize(string $value): string
|
||||
{
|
||||
return urlencode(Crypt::encryptString($value));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -35,4 +33,28 @@ class DownloadService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist validated download URLs for an episode. Sizes are written
|
||||
* directly from the CDN validation response, so no size job is dispatched.
|
||||
*
|
||||
* @param array<string, array{url: string, size?: float|int|null}> $downloads
|
||||
*/
|
||||
public function createOrUpdateDownloadsFromArray(Episode $episode, array $downloads): void
|
||||
{
|
||||
foreach ($downloads as $type => $data) {
|
||||
if (empty($data['url'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Downloads::updateOrCreate([
|
||||
'episode_id' => $episode->id,
|
||||
'type' => $type,
|
||||
], [
|
||||
'url' => $data['url'],
|
||||
'size' => $data['size'] ?? null,
|
||||
'validated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+125
-41
@@ -4,15 +4,16 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Hentai;
|
||||
use App\Models\ModLog;
|
||||
use App\Models\Studios;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
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;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class EpisodeService
|
||||
{
|
||||
@@ -24,44 +25,73 @@ class EpisodeService
|
||||
|
||||
if (is_numeric($lastPart) && $lastPart < 1000) {
|
||||
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
|
||||
|
||||
return implode('-', $slugParts);
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
public function createEpisode(
|
||||
Request $request,
|
||||
Hentai $hentai,
|
||||
int $episodeNumber,
|
||||
?Studios $studio = null,
|
||||
?Episode $referenceEpisode = null
|
||||
): Episode
|
||||
private function applyTags(Request $request, Episode $episode): void
|
||||
{
|
||||
$episode = new Episode();
|
||||
$episode->title = $referenceEpisode->title ?? $request->input('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;
|
||||
$episode->studios_id = $referenceEpisode->studio->id ?? $studio->id;
|
||||
$episode->episode = $episodeNumber;
|
||||
$episode->description = $referenceEpisode ? $request->input('description') : $request->input("description{$episodeNumber}");
|
||||
$episode->url = $referenceEpisode ? $request->input('baseurl') : rtrim($request->input('baseurl'), '/').'/E'.str_pad($episodeNumber, 2, '0', STR_PAD_LEFT);
|
||||
$episode->view_count = 0;
|
||||
$episode->interpolated = true;
|
||||
$episode->is_dvd_aspect = false;
|
||||
$episode->release_date = $referenceEpisode->release_date ?? Carbon::parse($request->input('releasedate'))->format('Y-m-d');
|
||||
$episode->cover_url = "/images/hentai/{$hentai->slug}/cover-ep-{$episodeNumber}.webp";
|
||||
$episode->save();
|
||||
|
||||
// Tagging
|
||||
$tags = $referenceEpisode ? $referenceEpisode->tags : json_decode($request->input('tags'));
|
||||
$tags = json_decode($request->input('tags'));
|
||||
$newtags = [];
|
||||
foreach ($tags as $t) {
|
||||
$episode->tag($referenceEpisode ? $t->name : $t->value);
|
||||
$newtags[] = $t->value;
|
||||
}
|
||||
|
||||
return $episode;
|
||||
$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
|
||||
@@ -77,17 +107,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,6 +140,41 @@ class EpisodeService
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an episode from an array of data (used by the Livewire release form).
|
||||
*
|
||||
* @param array{title: string, title_jpn: string, baseurl: string, description: string, releasedate: string, tags: string[], interpolated_uhd?: bool} $data
|
||||
*/
|
||||
public function createEpisodeFromArray(
|
||||
array $data,
|
||||
Hentai $hentai,
|
||||
int $episodeNumber,
|
||||
?Studios $studio = null
|
||||
): Episode {
|
||||
$episode = new Episode;
|
||||
$episode->title = $data['title'];
|
||||
$episode->title_search = preg_replace('/[^A-Za-z0-9 ]/', '', $episode->title);
|
||||
$episode->title_jpn = $data['title_jpn'];
|
||||
$episode->slug = "{$hentai->slug}-{$episodeNumber}";
|
||||
$episode->hentai_id = $hentai->id;
|
||||
$episode->studios_id = $studio->id;
|
||||
$episode->episode = $episodeNumber;
|
||||
$episode->description = $data['description'];
|
||||
$episode->url = rtrim($data['baseurl'], '/').'/E'.str_pad($episodeNumber, 2, '0', STR_PAD_LEFT);
|
||||
$episode->view_count = 0;
|
||||
$episode->interpolated = true;
|
||||
$episode->interpolated_uhd = $data['interpolated_uhd'] ?? false;
|
||||
$episode->is_dvd_aspect = false;
|
||||
$episode->release_date = Carbon::parse($data['releasedate'])->format('Y-m-d');
|
||||
$episode->cover_url = "/images/hentai/{$hentai->slug}/cover-ep-{$episodeNumber}.webp";
|
||||
$episode->save();
|
||||
|
||||
foreach ($data['tags'] as $tag) {
|
||||
$episode->tag($tag);
|
||||
}
|
||||
|
||||
return $episode;
|
||||
}
|
||||
|
||||
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
|
||||
{
|
||||
@@ -103,15 +182,20 @@ class EpisodeService
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveCoverFromFile($episode, $slug, $episodeNumber, $request->file("episodecover{$episodeNumber}"));
|
||||
}
|
||||
|
||||
public function saveCoverFromFile(Episode $episode, string $slug, int $episodeNumber, UploadedFile|TemporaryUploadedFile $file): void
|
||||
{
|
||||
// Create Folder for Image Upload
|
||||
if (! Storage::disk('public')->exists("/images/hentai/{$slug}")) {
|
||||
Storage::disk('public')->makeDirectory("/images/hentai/{$slug}");
|
||||
}
|
||||
|
||||
// Encode and save cover image
|
||||
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
|
||||
Image::read($file->getRealPath())
|
||||
->cover(268, 394)
|
||||
->encode(new WebpEncoder())
|
||||
->encode(new WebpEncoder)
|
||||
->save(Storage::disk('public')->path($episode->cover_url));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,40 @@ 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\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
use Intervention\Image\Encoders\WebpEncoder;
|
||||
use Intervention\Image\Laravel\Facades\Image;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
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->saveGalleryFiles($hentai, $episode, $episodeNumber, $request->file('episodegallery'.$galleryInputNumber));
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteOldGallery($episode);
|
||||
/**
|
||||
* Save a gallery for the given episode from a list of uploaded files.
|
||||
*
|
||||
* @param array<int, UploadedFile|TemporaryUploadedFile> $files
|
||||
*/
|
||||
public function saveGalleryFiles(Hentai $hentai, Episode $episode, int $episodeNumber, array $files): void
|
||||
{
|
||||
$this->deleteOldGallery($episode);
|
||||
|
||||
$this->createGalleryFolder($hentai);
|
||||
$this->createGalleryFolder($hentai);
|
||||
|
||||
$counter = 0;
|
||||
foreach($request->file('episodegallery'.$galleryInputNumber) as $file) {
|
||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||
$this->saveGalleryImage($gallery, $file);
|
||||
$counter += 1;
|
||||
}
|
||||
$counter = 0;
|
||||
foreach ($files as $file) {
|
||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||
$this->saveGalleryImage($gallery, $file);
|
||||
$counter += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +52,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";
|
||||
@@ -55,16 +62,16 @@ class GalleryService
|
||||
return $gallery;
|
||||
}
|
||||
|
||||
private function saveGalleryImage(Gallery $gallery, $sourceImage): void
|
||||
private function saveGalleryImage(Gallery $gallery, UploadedFile|TemporaryUploadedFile $sourceImage): void
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"agents": [
|
||||
"cursor"
|
||||
],
|
||||
"cloud": false,
|
||||
"guidelines": true,
|
||||
"mcp": true,
|
||||
"nightwatch": false,
|
||||
"sail": false,
|
||||
"skills": [
|
||||
"laravel-best-practices",
|
||||
"scout-development",
|
||||
"socialite-development",
|
||||
"livewire-development",
|
||||
"tailwindcss-development"
|
||||
]
|
||||
}
|
||||
+9
-4
@@ -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
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
+3
-1
@@ -9,7 +9,7 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"grantholle/laravel-altcha": "^2.1",
|
||||
"altcha-org/altcha": "^2.0",
|
||||
"guzzlehttp/guzzle": "^7.8.1",
|
||||
"hisorange/browser-detect": "^5.0",
|
||||
"http-interop/http-factory-guzzle": "^1.2",
|
||||
@@ -28,11 +28,13 @@
|
||||
"rtconner/laravel-tagging": "^5.0",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"spatie/laravel-discord-alerts": "^1.8",
|
||||
"spatie/laravel-passkeys": "^1.7",
|
||||
"spatie/laravel-sitemap": "^7.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.16",
|
||||
"fakerphp/faker": "^1.24.0",
|
||||
"laravel/boost": "^2.4",
|
||||
"laravel/breeze": "^2.3",
|
||||
"laravel/pint": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
|
||||
Generated
+2088
-757
File diff suppressed because it is too large
Load Diff
+11
-6
@@ -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 [
|
||||
|
||||
@@ -116,7 +121,7 @@ return [
|
||||
| Supported Locales
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is used to display the supported locales by this app, it also is
|
||||
| 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
|
||||
|
|
||||
*/
|
||||
@@ -175,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(),
|
||||
|
||||
/*
|
||||
@@ -195,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
@@ -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' => [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user