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
|
||||||
.env.backup
|
.env.backup
|
||||||
.env.production
|
.env.production
|
||||||
|
.env.testing
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
@@ -21,3 +22,5 @@ yarn-error.log
|
|||||||
/.idea
|
/.idea
|
||||||
/.vscode
|
/.vscode
|
||||||
.directory
|
.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
|
## hstream Website
|
||||||
|
|
||||||
### Install
|
### Install (Ubuntu)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install PHP
|
# Install PHP
|
||||||
sudo add-apt-repository ppa:ondrej/php
|
sudo add-apt-repository ppa:ondrej/php
|
||||||
apt update && apt upgrade
|
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
|
# Install NodeJS
|
||||||
curl -sL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh
|
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)
|
# Install NGINX (skip for local dev)
|
||||||
apt install nginx
|
apt install nginx
|
||||||
apt install php8.3-fpm
|
apt install php8.4-fpm
|
||||||
|
|
||||||
# Install MariaDB
|
# Install MariaDB
|
||||||
apt install mariadb-server
|
apt install mariadb-server
|
||||||
@@ -54,7 +54,7 @@ nano /etc/supervisor/conf.d/laravel-queue.conf :
|
|||||||
|
|
||||||
[program:laravel-queue]
|
[program:laravel-queue]
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
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
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
@@ -83,9 +83,9 @@ zip -r hstream_2023_11_30.zip hstream/
|
|||||||
|
|
||||||
### Update
|
### Update
|
||||||
```bash
|
```bash
|
||||||
php artisan down
|
php84 artisan down
|
||||||
git pull
|
git pull
|
||||||
npm run build
|
npm run build
|
||||||
php artisan view:clear && php artisan optimize:clear && php artisan cache:clear && service php8.4-fpm restart
|
php84 artisan view:clear && php84 artisan optimize:clear && php84 artisan cache:clear && service php8.4-fpm restart
|
||||||
php artisan up
|
php84 artisan up
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\PopularDaily;
|
use App\Models\PopularDaily;
|
||||||
use App\Models\PopularWeekly;
|
|
||||||
use App\Models\PopularMonthly;
|
use App\Models\PopularMonthly;
|
||||||
|
use App\Models\PopularWeekly;
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class AutoStats extends Command
|
class AutoStats extends Command
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Console\Commands;
|
|||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Spatie\Sitemap\Sitemap;
|
use Spatie\Sitemap\Sitemap;
|
||||||
@@ -17,7 +16,7 @@ class GenerateSitemap extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'sitemap:generate';
|
protected $signature = 'app:generate-sitemap';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Downloads;
|
|
||||||
use App\Jobs\GetFileSizeFromCDN;
|
use App\Jobs\GetFileSizeFromCDN;
|
||||||
|
use App\Models\Downloads;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class GetFileSize extends Command
|
class GetFileSize extends Command
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ namespace App\Console\Commands;
|
|||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserDownload;
|
use App\Models\UserDownload;
|
||||||
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class ResetUserDownloads extends Command
|
class ResetUserDownloads extends Command
|
||||||
{
|
{
|
||||||
|
|||||||
+114
-17
@@ -3,23 +3,24 @@
|
|||||||
namespace App\Helpers;
|
namespace App\Helpers;
|
||||||
|
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
|
use App\Models\Downloads;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
|
use App\Models\PopularDaily;
|
||||||
use App\Models\PopularMonthly;
|
use App\Models\PopularMonthly;
|
||||||
use App\Models\PopularWeekly;
|
use App\Models\PopularWeekly;
|
||||||
use App\Models\PopularDaily;
|
use App\Models\User;
|
||||||
|
|
||||||
use Conner\Tagging\Model\Tag;
|
use Conner\Tagging\Model\Tag;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class CacheHelper
|
class CacheHelper
|
||||||
{
|
{
|
||||||
public static function getRecentlyReleased(bool $guest)
|
public static function getRecentlyReleased(bool $guest)
|
||||||
{
|
{
|
||||||
$guestString = $guest ? 'guest' : 'authed';
|
$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')
|
return Episode::with('gallery')
|
||||||
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
||||||
->orderBy('release_date', 'desc')
|
->orderBy('release_date', 'desc')
|
||||||
@@ -31,7 +32,8 @@ class CacheHelper
|
|||||||
public static function getRecentlyUploaded(bool $guest)
|
public static function getRecentlyUploaded(bool $guest)
|
||||||
{
|
{
|
||||||
$guestString = $guest ? 'guest' : 'authed';
|
$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')
|
return Episode::with('gallery')
|
||||||
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
@@ -42,29 +44,124 @@ class CacheHelper
|
|||||||
|
|
||||||
public static function getTotalViewCount()
|
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');
|
return Episode::sum('view_count');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTotalEpisodeCount()
|
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();
|
return Episode::count();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getTotalHentaiCount()
|
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();
|
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)
|
public static function getPopularAllTime(bool $guest)
|
||||||
{
|
{
|
||||||
$guestString = $guest ? 'guest' : 'authed';
|
$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')
|
return Episode::with('gallery')
|
||||||
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
||||||
->orderBy('view_count', 'desc')
|
->orderBy('view_count', 'desc')
|
||||||
@@ -75,7 +172,7 @@ class CacheHelper
|
|||||||
|
|
||||||
public static function getPopularMonthly()
|
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')
|
return PopularMonthly::groupBy('episode_id')
|
||||||
->select('episode_id', DB::raw('count(*) as total'))
|
->select('episode_id', DB::raw('count(*) as total'))
|
||||||
->with('episode.gallery')
|
->with('episode.gallery')
|
||||||
@@ -87,7 +184,7 @@ class CacheHelper
|
|||||||
|
|
||||||
public static function getPopularWeekly()
|
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')
|
return PopularWeekly::groupBy('episode_id')
|
||||||
->select('episode_id', DB::raw('count(*) as total'))
|
->select('episode_id', DB::raw('count(*) as total'))
|
||||||
->with('episode.gallery')
|
->with('episode.gallery')
|
||||||
@@ -100,7 +197,7 @@ class CacheHelper
|
|||||||
|
|
||||||
public static function getPopularDaily()
|
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')
|
return PopularDaily::groupBy('episode_id')
|
||||||
->select('episode_id', DB::raw('count(*) as total'))
|
->select('episode_id', DB::raw('count(*) as total'))
|
||||||
->with('episode.gallery')
|
->with('episode.gallery')
|
||||||
@@ -112,22 +209,22 @@ class CacheHelper
|
|||||||
|
|
||||||
public static function getMostLikes()
|
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();
|
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()
|
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();
|
return Tag::where('count', '>', 0)->orderBy('slug', 'ASC')->get();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getLatestComments()
|
public static function getLatestComments()
|
||||||
{
|
{
|
||||||
return Cache::remember("latest_comments", now()->addMinutes(60), function () {
|
return Cache::remember('latest_comments', now()->addMinutes(60), function () {
|
||||||
return Comment::latest()->take(10)->get();
|
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()
|
public static function shortCommit()
|
||||||
{
|
{
|
||||||
return Cache::remember("git_commit", now()->addMinutes(60), function () {
|
return Cache::remember('git_commit', now()->addMinutes(60), function () {
|
||||||
try {
|
try {
|
||||||
return trim(exec('git rev-parse --short HEAD'));
|
return trim(exec('git rev-parse --short HEAD'));
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Alert;
|
use App\Models\Alert;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class AlertController extends Controller
|
class AlertController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display alert index page
|
* Display alert index page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.alert.index');
|
return view('admin.alert.index');
|
||||||
}
|
}
|
||||||
@@ -19,7 +21,7 @@ class AlertController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create Alert.
|
* Create Alert.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'message' => 'required|string|max:255',
|
'message' => 'required|string|max:255',
|
||||||
@@ -39,7 +41,7 @@ class AlertController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete Alert.
|
* Delete Alert.
|
||||||
*/
|
*/
|
||||||
public function delete(int $alert_id): \Illuminate\Http\RedirectResponse
|
public function delete(int $alert_id): RedirectResponse
|
||||||
{
|
{
|
||||||
Alert::where('id', $alert_id)->delete();
|
Alert::where('id', $alert_id)->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class CommentsController extends Controller
|
class CommentsController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Comments Page.
|
* Display Comments Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.comments.index');
|
return view('admin.comments.index');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,27 @@ namespace App\Http\Controllers\Admin;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ContactController extends Controller
|
class ContactController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Contact Page.
|
* Display Contact Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$contacts = Contact::orderBy('created_at', 'DESC')->get();
|
$contacts = Contact::orderBy('created_at', 'DESC')->get();
|
||||||
|
|
||||||
return view('admin.contact.index', [
|
return view('admin.contact.index', [
|
||||||
'contacts' => $contacts
|
'contacts' => $contacts,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete Contact.
|
* Delete Contact.
|
||||||
*/
|
*/
|
||||||
public function delete(int $contact_id): \Illuminate\Http\RedirectResponse
|
public function delete(int $contact_id): RedirectResponse
|
||||||
{
|
{
|
||||||
Contact::where('id', $contact_id)->delete();
|
Contact::where('id', $contact_id)->delete();
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,22 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Episode;
|
|
||||||
|
|
||||||
use App\Jobs\DiscordReleaseNotification;
|
use App\Jobs\DiscordReleaseNotification;
|
||||||
|
use App\Models\Episode;
|
||||||
use App\Services\DownloadService;
|
use App\Services\DownloadService;
|
||||||
use App\Services\EpisodeService;
|
use App\Services\EpisodeService;
|
||||||
use App\Services\GalleryService;
|
use App\Services\GalleryService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class EpisodeController extends Controller
|
class EpisodeController extends Controller
|
||||||
{
|
{
|
||||||
protected EpisodeService $episodeService;
|
protected EpisodeService $episodeService;
|
||||||
|
|
||||||
protected GalleryService $galleryService;
|
protected GalleryService $galleryService;
|
||||||
|
|
||||||
protected DownloadService $downloadService;
|
protected DownloadService $downloadService;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -28,39 +31,22 @@ class EpisodeController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add Episode to existing series
|
* Edit Episode
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$referenceEpisode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
$episodeNumber = $referenceEpisode->hentai->episodes()->count() + 1;
|
|
||||||
|
|
||||||
// Create Episode
|
if ($request->user()->hasRole(UserRole::MODERATOR)) {
|
||||||
$episode = $this->episodeService->createEpisode($request, $referenceEpisode->hentai, $episodeNumber, null, $referenceEpisode);
|
$this->episodeService->updateEpisodeModerator($request, $episode->id);
|
||||||
$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();
|
cache()->flush();
|
||||||
|
|
||||||
return to_route('hentai.index', [
|
return to_route('hentai.index', [
|
||||||
'title' => $episode->slug
|
'title' => $episode->slug,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit Episode
|
|
||||||
*/
|
|
||||||
public function update(Request $request): \Illuminate\Http\RedirectResponse
|
|
||||||
{
|
|
||||||
$episode = Episode::with('hentai')->where('id', $request->input('episode_id'))->firstOrFail();
|
|
||||||
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
$studio = $this->episodeService->getOrCreateStudio(json_decode($request->input('studio'))[0]->value);
|
||||||
|
|
||||||
$oldinterpolated = $episode->interpolated;
|
$oldinterpolated = $episode->interpolated;
|
||||||
@@ -87,7 +73,7 @@ class EpisodeController extends Controller
|
|||||||
cache()->flush();
|
cache()->flush();
|
||||||
|
|
||||||
return to_route('hentai.index', [
|
return to_route('hentai.index', [
|
||||||
'title' => $episode->slug
|
'title' => $episode->slug,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,86 +3,15 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Hentai;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
use App\Jobs\DiscordReleaseNotification;
|
|
||||||
use App\Services\DownloadService;
|
|
||||||
use App\Services\EpisodeService;
|
|
||||||
use App\Services\GalleryService;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ReleaseController extends Controller
|
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
|
* Display release page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.release.create');
|
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;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use App\Models\SiteBackground;
|
use App\Models\SiteBackground;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Intervention\Image\Laravel\Facades\Image;
|
use Illuminate\View\View;
|
||||||
use Intervention\Image\Encoders\WebpEncoder;
|
use Intervention\Image\Encoders\WebpEncoder;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
|
||||||
class SiteBackgroundController extends Controller
|
class SiteBackgroundController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display admin index page
|
* Display admin index page
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.background.index', [
|
return view('admin.background.index', [
|
||||||
'images' => SiteBackground::all(),
|
'images' => SiteBackground::all(),
|
||||||
@@ -28,7 +28,7 @@ class SiteBackgroundController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create new site backgrounds
|
* Create new site backgrounds
|
||||||
*/
|
*/
|
||||||
public function create(Request $request): \Illuminate\Http\RedirectResponse
|
public function create(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'images' => 'required',
|
'images' => 'required',
|
||||||
@@ -44,7 +44,7 @@ class SiteBackgroundController extends Controller
|
|||||||
$bg = SiteBackground::create(array_merge(
|
$bg = SiteBackground::create(array_merge(
|
||||||
$request->only(['date_start', 'date_end']),
|
$request->only(['date_start', 'date_end']),
|
||||||
[
|
[
|
||||||
'default' => (bool) $request->input('default', false)
|
'default' => (bool) $request->input('default', false),
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -55,13 +55,14 @@ class SiteBackgroundController extends Controller
|
|||||||
|
|
||||||
Image::read($file->getRealPath())
|
Image::read($file->getRealPath())
|
||||||
->scaleDown(height: $resolution)
|
->scaleDown(height: $resolution)
|
||||||
->encode(new WebpEncoder())
|
->encode(new WebpEncoder)
|
||||||
->save(public_path($targetPath));
|
->save(public_path($targetPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
Log::error($e->getMessage());
|
Log::error($e->getMessage());
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class SiteBackgroundController extends Controller
|
|||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(Request $request): \Illuminate\Http\RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'id' => 'required|exists:site_backgrounds,id',
|
'id' => 'required|exists:site_backgrounds,id',
|
||||||
@@ -85,7 +86,7 @@ class SiteBackgroundController extends Controller
|
|||||||
SiteBackground::where('id', $request->input('id'))->update(array_merge(
|
SiteBackground::where('id', $request->input('id'))->update(array_merge(
|
||||||
$request->only(['date_start', 'date_end']),
|
$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
|
* Delete backround
|
||||||
*/
|
*/
|
||||||
public function delete(Request $request): \Illuminate\Http\RedirectResponse
|
public function delete(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$id = $request->input('id');
|
$id = $request->input('id');
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ class SiteBackgroundController extends Controller
|
|||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
Log::error($e->getMessage());
|
Log::error($e->getMessage());
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\EpisodeSubtitle;
|
use App\Models\EpisodeSubtitle;
|
||||||
use App\Models\Subtitle;
|
use App\Models\Subtitle;
|
||||||
use App\Http\Controllers\Controller;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SubtitleController extends Controller
|
class SubtitleController extends Controller
|
||||||
@@ -13,7 +14,7 @@ class SubtitleController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Add new Subtitle.
|
* Add new Subtitle.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$subtitle = Subtitle::create([
|
$subtitle = Subtitle::create([
|
||||||
'name' => $request->name,
|
'name' => $request->name,
|
||||||
@@ -32,7 +33,7 @@ class SubtitleController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Update Episode Subtitles.
|
* 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();
|
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
namespace App\Http\Controllers\Admin;
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
use App\Models\User;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Users Page.
|
* Display Users Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('admin.users.index');
|
return view('admin.users.index');
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,6 @@ class UserController extends Controller
|
|||||||
'action' => 'required',
|
'action' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
$user = User::findOrFail($validated['id']);
|
$user = User::findOrFail($validated['id']);
|
||||||
|
|
||||||
switch ($validated['action']) {
|
switch ($validated['action']) {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Helpers\CacheHelper;
|
use App\Helpers\CacheHelper;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Studios;
|
use App\Models\Studios;
|
||||||
use App\Models\Subtitle;
|
use App\Models\Subtitle;
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class AdminApiController extends Controller
|
class AdminApiController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Downloads;
|
use App\Models\Downloads;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
|
||||||
|
|
||||||
class DownloadApiController extends Controller
|
class DownloadApiController extends Controller
|
||||||
{
|
{
|
||||||
@@ -19,7 +17,7 @@ class DownloadApiController extends Controller
|
|||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'episode_id' => ['required'],
|
'episode_id' => ['required'],
|
||||||
'captcha' => ['required', new ValidAltcha],
|
'captcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$episode = Episode::where('id', $request->input('episode_id'))
|
$episode = Episode::where('id', $request->input('episode_id'))
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
use App\Models\PopularMonthly;
|
use App\Models\PopularMonthly;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class HentaiApiController extends Controller
|
class HentaiApiController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,10 +3,15 @@
|
|||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use App\Models\VideoEngagement;
|
||||||
|
use App\Models\Watched;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
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
|
class StreamApiController extends Controller
|
||||||
{
|
{
|
||||||
@@ -33,7 +38,106 @@ class StreamApiController extends Controller
|
|||||||
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
|
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
|
||||||
'stream_domains' => config('hstream.stream_domain'),
|
'stream_domains' => config('hstream.stream_domain'),
|
||||||
'asia_stream_domains' => config('hstream.asia_stream_domain'),
|
'asia_stream_domains' => config('hstream.asia_stream_domain'),
|
||||||
'extra_subtitles' => $subtitles
|
'extra_subtitles' => $subtitles,
|
||||||
], 200);
|
], 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\Helpers\CacheHelper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
use Conner\Tagging\Model\Tag;
|
use Conner\Tagging\Model\Tag;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class UserApiController extends Controller
|
class UserApiController extends Controller
|
||||||
{
|
{
|
||||||
@@ -33,11 +32,10 @@ class UserApiController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'success',
|
'message' => 'success',
|
||||||
'tags' => $tagWhiteList,
|
'tags' => $tagWhiteList,
|
||||||
'usertags' => $tagBlackList
|
'usertags' => $tagBlackList,
|
||||||
], 200);
|
], 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;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
use App\Models\User;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Laravel\Socialite\Facades\Socialite;
|
use Laravel\Socialite\Facades\Socialite;
|
||||||
|
use Laravel\Socialite\Two\InvalidStateException;
|
||||||
|
|
||||||
class DiscordAuthController extends Controller
|
class DiscordAuthController extends Controller
|
||||||
{
|
{
|
||||||
@@ -28,7 +27,12 @@ class DiscordAuthController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function callback(): RedirectResponse
|
public function callback(): RedirectResponse
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
$discordUser = Socialite::driver('discord')->user();
|
$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();
|
$user = User::where('discord_id', $discordUser->id)->first();
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ class DiscordAuthController extends Controller
|
|||||||
// User is not in the guild
|
// User is not in the guild
|
||||||
if ($response->status() === 404) {
|
if ($response->status() === 404) {
|
||||||
$user->removeRole(UserRole::SUPPORTER);
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +119,7 @@ class DiscordAuthController extends Controller
|
|||||||
if (! $hasSupporterRole) {
|
if (! $hasSupporterRole) {
|
||||||
// Remove role if not found
|
// Remove role if not found
|
||||||
$user->removeRole(UserRole::SUPPORTER);
|
$user->removeRole(UserRole::SUPPORTER);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Hash;
|
|||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class NewPasswordController extends Controller
|
class NewPasswordController extends Controller
|
||||||
@@ -26,7 +27,7 @@ class NewPasswordController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming new password request.
|
* Handle an incoming new password request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ class PasswordController extends Controller
|
|||||||
public function update(Request $request): RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
// If user logged in with Discord and has not yet a password, allow to set password
|
// 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', [
|
$validated = $request->validateWithBag('updatePassword', [
|
||||||
'password' => ['required', Password::defaults(), 'confirmed'],
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
|||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class PasswordResetLinkController extends Controller
|
class PasswordResetLinkController extends Controller
|
||||||
@@ -21,7 +22,7 @@ class PasswordResetLinkController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming password reset link request.
|
* Handle an incoming password reset link request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ namespace App\Http\Controllers\Auth;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Validation\Rules;
|
use Illuminate\Validation\Rules;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
|
||||||
|
|
||||||
class RegisteredUserController extends Controller
|
class RegisteredUserController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming registration request.
|
* Handle an incoming registration request.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
@@ -26,7 +26,7 @@ class RegisteredUserController extends Controller
|
|||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::create([
|
$user = User::create([
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
|
||||||
|
|
||||||
class ContactController extends Controller
|
class ContactController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Contact Page.
|
* Display Contact Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('contact.form');
|
return view('contact.form');
|
||||||
}
|
}
|
||||||
@@ -20,17 +21,17 @@ class ContactController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Store Contact Submission.
|
* Store Contact Submission.
|
||||||
*/
|
*/
|
||||||
public function store(Request $request): \Illuminate\Http\RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
'email' => 'required|max:50',
|
'email' => 'required|max:50',
|
||||||
'message' => 'required|max:1000',
|
'message' => 'required|max:1000',
|
||||||
'subject' => 'required|max:50',
|
'subject' => 'required|max:50',
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$contact = new Contact();
|
$contact = new Contact;
|
||||||
$contact->name = $request->input('name');
|
$contact->name = $request->input('name');
|
||||||
$contact->email = $request->input('email');
|
$contact->email = $request->input('email');
|
||||||
$contact->message = $request->input('message');
|
$contact->message = $request->input('message');
|
||||||
|
|||||||
@@ -2,32 +2,32 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Episode;
|
|
||||||
use App\Helpers\CacheHelper;
|
use App\Helpers\CacheHelper;
|
||||||
|
use App\Models\Episode;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Cookie;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Home Page.
|
* Display Home Page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$guest = Auth::guest();
|
$guest = Auth::guest();
|
||||||
|
|
||||||
$guestString = $guest ? 'guest' : 'authed';
|
$guestString = $guest ? 'guest' : 'authed';
|
||||||
|
|
||||||
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () =>
|
$mostLikes = \cache()->remember('mostLikes'.$guestString, 300, fn () => Episode::with('gallery')
|
||||||
Episode::with('gallery')
|
|
||||||
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
->when($guest, fn ($query) => $query->withoutTags(['loli', 'shota']))
|
||||||
->whereIn('id', function ($query) {
|
->whereIn('id', function ($query) {
|
||||||
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
|
$mostLikesIds = CacheHelper::getMostLikes()->pluck('markable_id')->toArray();
|
||||||
$query->selectRaw('id')
|
$query->selectRaw('id')
|
||||||
->from('episodes')
|
->from('episodes')
|
||||||
->whereIn('id', $mostLikesIds)
|
->whereIn('id', $mostLikesIds)
|
||||||
->orderByRaw("FIELD(id, " . implode(',', $mostLikesIds) . ")");
|
->orderByRaw('FIELD(id, '.implode(',', $mostLikesIds).')');
|
||||||
})
|
})
|
||||||
->get()
|
->get()
|
||||||
);
|
);
|
||||||
@@ -47,7 +47,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Banned Page.
|
* Display Banned Page.
|
||||||
*/
|
*/
|
||||||
public function banned(): \Illuminate\View\View
|
public function banned(): View
|
||||||
{
|
{
|
||||||
return view('auth.banned');
|
return view('auth.banned');
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ class HomeController extends Controller
|
|||||||
* Redirects to a random Hentai episode
|
* Redirects to a random Hentai episode
|
||||||
* Done due to performance reasons
|
* Done due to performance reasons
|
||||||
*/
|
*/
|
||||||
public function random(): \Illuminate\Http\RedirectResponse
|
public function random(): RedirectResponse
|
||||||
{
|
{
|
||||||
$random = Episode::inRandomOrder()
|
$random = Episode::inRandomOrder()
|
||||||
->limit(1)
|
->limit(1)
|
||||||
@@ -71,7 +71,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Search Page.
|
* Display Search Page.
|
||||||
*/
|
*/
|
||||||
public function search(): \Illuminate\View\View
|
public function search(): View
|
||||||
{
|
{
|
||||||
return view('search.index');
|
return view('search.index');
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Download Search Page.
|
* Display Download Search Page.
|
||||||
*/
|
*/
|
||||||
public function downloadSearch(): \Illuminate\View\View
|
public function downloadSearch(): View
|
||||||
{
|
{
|
||||||
return view('search.download');
|
return view('search.download');
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Redirect POST Data to GET with Query String.
|
* 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', [
|
return redirect()->route('hentai.search', [
|
||||||
'search' => $request->input('live-search'),
|
'search' => $request->input('live-search'),
|
||||||
@@ -97,19 +97,31 @@ class HomeController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display Stats Page.
|
* Display Stats Page.
|
||||||
*/
|
*/
|
||||||
public function stats(): \Illuminate\View\View
|
public function stats(): View
|
||||||
{
|
{
|
||||||
return view('home.stats', [
|
return view('home.stats', [
|
||||||
'viewCount' => CacheHelper::getTotalViewCount(),
|
'viewCount' => CacheHelper::getTotalViewCount(),
|
||||||
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
|
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
|
||||||
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
|
'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
|
* 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);
|
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,15 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class NotificationController extends Controller
|
class NotificationController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display the user's notification page.
|
* Display the user's notification page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request): \Illuminate\View\View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
return view('profile.notifications', [
|
return view('profile.notifications', [
|
||||||
'user' => $request->user(),
|
'user' => $request->user(),
|
||||||
@@ -19,10 +20,17 @@ class NotificationController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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([
|
$request->validate([
|
||||||
'id' => 'required|exists:notifications,id',
|
'id' => 'required|exists:notifications,id',
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ use App\Models\Episode;
|
|||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
use App\Models\PlaylistEpisode;
|
use App\Models\PlaylistEpisode;
|
||||||
use App\Services\PlaylistService;
|
use App\Services\PlaylistService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use RealRashid\SweetAlert\Facades\Alert;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class PlaylistController extends Controller
|
class PlaylistController extends Controller
|
||||||
{
|
{
|
||||||
@@ -21,7 +23,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display the public playlists page.
|
* Display the public playlists page.
|
||||||
*/
|
*/
|
||||||
public function index(): \Illuminate\View\View
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('playlist.index');
|
return view('playlist.index');
|
||||||
}
|
}
|
||||||
@@ -29,7 +31,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display public playlist.
|
* 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);
|
abort(404);
|
||||||
@@ -42,14 +44,13 @@ class PlaylistController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the user's playlists page.
|
* Display the user's playlists page.
|
||||||
*/
|
*/
|
||||||
public function playlists(Request $request): \Illuminate\View\View
|
public function playlists(Request $request): View
|
||||||
{
|
{
|
||||||
$title = 'Delete Playlist!';
|
$title = 'Delete Playlist!';
|
||||||
$text = "Are you sure you want to delete?";
|
$text = 'Are you sure you want to delete?';
|
||||||
confirmDelete($title, $text);
|
confirmDelete($title, $text);
|
||||||
|
|
||||||
return view('profile.playlists', [
|
return view('profile.playlists', [
|
||||||
@@ -61,7 +62,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display user's playlist.
|
* 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);
|
abort(404);
|
||||||
@@ -79,13 +80,13 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Create user playlist (Form).
|
* Create user playlist (Form).
|
||||||
*/
|
*/
|
||||||
public function createPlaylist(Request $request): \Illuminate\Http\RedirectResponse
|
public function createPlaylist(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$playlist = new Playlist();
|
$playlist = new Playlist;
|
||||||
$playlist->user_id = $request->user()->id;
|
$playlist->user_id = $request->user()->id;
|
||||||
$playlist->name = $request->input('name');
|
$playlist->name = $request->input('name');
|
||||||
$playlist->is_private = $request->input('visiblity') === 'private';
|
$playlist->is_private = $request->input('visiblity') === 'private';
|
||||||
@@ -94,10 +95,38 @@ class PlaylistController extends Controller
|
|||||||
return to_route('profile.playlists');
|
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.
|
* 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);
|
abort(404);
|
||||||
@@ -117,7 +146,7 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete episode from playlist.
|
* 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([
|
return response()->json([
|
||||||
@@ -145,13 +174,13 @@ class PlaylistController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Add to user playlist (API).
|
* Add to user playlist (API).
|
||||||
*/
|
*/
|
||||||
public function addPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
|
public function addPlaylistApi(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'playlist' => 'required|max:30',
|
'playlist' => 'required|max:30',
|
||||||
'episode_id' => 'required'
|
'episode_id' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$playlist = Playlist::where('user_id', $user->id)->where('id', $request->input('playlist'))->firstOrFail();
|
$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();
|
$exists = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->exists();
|
||||||
if ($exists) {
|
if ($exists) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'already-added'
|
'message' => 'already-added',
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,20 +204,20 @@ class PlaylistController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'success'
|
'message' => 'success',
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create user playlist (API).
|
* Create user playlist (API).
|
||||||
*/
|
*/
|
||||||
public function createPlaylistApi(Request $request): \Illuminate\Http\JsonResponse
|
public function createPlaylistApi(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validate([
|
$validated = $request->validate([
|
||||||
'name' => 'required|max:30',
|
'name' => 'required|max:30',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$playlist = new Playlist();
|
$playlist = new Playlist;
|
||||||
$playlist->user_id = $request->user()->id;
|
$playlist->user_id = $request->user()->id;
|
||||||
$playlist->name = $request->input('name');
|
$playlist->name = $request->input('name');
|
||||||
$playlist->is_private = $request->input('visiblity') === 'private';
|
$playlist->is_private = $request->input('visiblity') === 'private';
|
||||||
@@ -196,7 +225,7 @@ class PlaylistController extends Controller
|
|||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'success',
|
'message' => 'success',
|
||||||
'playlist_id' => $playlist->id
|
'playlist_id' => $playlist->id,
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,21 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ProfileUpdateRequest;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Http\Requests\ProfileUpdateRequest;
|
use Conner\Tagging\Model\Tag;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Redirect;
|
use Illuminate\Support\Facades\Redirect;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
use Intervention\Image\Laravel\Facades\Image;
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
|
||||||
use Conner\Tagging\Model\Tag;
|
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -121,6 +120,7 @@ class ProfileController extends Controller
|
|||||||
if (! $tags) {
|
if (! $tags) {
|
||||||
$user->tag_blacklist = null;
|
$user->tag_blacklist = null;
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
return Redirect::route('profile.settings')->with('status', 'blacklist-updated');
|
return Redirect::route('profile.settings')->with('status', 'blacklist-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Delete the user's account.
|
* Delete the user's account.
|
||||||
*/
|
*/
|
||||||
public function destroy(Request $request): \Illuminate\Http\RedirectResponse
|
public function destroy(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
@@ -174,11 +174,11 @@ class ProfileController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Store custom user avatar.
|
* 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
|
// Create Folder for Image Upload
|
||||||
if (! Storage::disk('public')->exists("/images/avatars")) {
|
if (! Storage::disk('public')->exists('/images/avatars')) {
|
||||||
Storage::disk('public')->makeDirectory("/images/avatars");
|
Storage::disk('public')->makeDirectory('/images/avatars');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete old avatar if it exists
|
// Delete old avatar if it exists
|
||||||
@@ -196,5 +196,4 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$user->avatar = $filename;
|
$user->avatar = $filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,23 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Helpers\CacheHelper;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Gallery;
|
use App\Models\Gallery;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
use App\Models\PlaylistEpisode;
|
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 hisorange\BrowserDetect\Facade as Browser;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class StreamController extends Controller
|
class StreamController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Display Stream Page.
|
* Display Stream Page.
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, string $title): \Illuminate\View\View
|
public function index(Request $request, string $title): View
|
||||||
{
|
{
|
||||||
$titleParts = explode('-', $title);
|
$titleParts = explode('-', $title);
|
||||||
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
|
if (! is_numeric($titleParts[array_key_last($titleParts)])) {
|
||||||
@@ -37,7 +34,6 @@ class StreamController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$episode = Episode::where('slug', $title)->firstOrFail();
|
$episode = Episode::where('slug', $title)->firstOrFail();
|
||||||
$gallery = Gallery::where('episode_id', $episode->id)->get();
|
$gallery = Gallery::where('episode_id', $episode->id)->get();
|
||||||
$moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get();
|
$moreEpisodes = Episode::with(['gallery', 'studio'])->where('hentai_id', $episode->hentai_id)->whereNot('id', $episode->id)->get();
|
||||||
@@ -54,32 +50,17 @@ class StreamController extends Controller
|
|||||||
// Increment Popular Count
|
// Increment Popular Count
|
||||||
$episode->incrementPopularCount();
|
$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
|
// Mobile Detection
|
||||||
$isMobile = Browser::isMobile();
|
$isMobile = Browser::isMobile();
|
||||||
|
|
||||||
// Playlist
|
// Playlist
|
||||||
if ($request->has('playlist')) {
|
if ($request->has('playlist')) {
|
||||||
// Get and check if playlist exists
|
// 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
|
// Check if episode is in playlist
|
||||||
$inPlaylist = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->firstOrFail();
|
$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
|
// 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);
|
abort(404);
|
||||||
@@ -91,7 +72,6 @@ class StreamController extends Controller
|
|||||||
'studioEpisodes' => $studioEpisodes,
|
'studioEpisodes' => $studioEpisodes,
|
||||||
'gallery' => $gallery,
|
'gallery' => $gallery,
|
||||||
'playlist' => $playlist,
|
'playlist' => $playlist,
|
||||||
'playlistEpisodes' => $playlistEpisodes,
|
|
||||||
'popularWeekly' => CacheHelper::getPopularWeekly(),
|
'popularWeekly' => CacheHelper::getPopularWeekly(),
|
||||||
'isMobile' => $isMobile,
|
'isMobile' => $isMobile,
|
||||||
]);
|
]);
|
||||||
|
|||||||
+56
-29
@@ -2,7 +2,34 @@
|
|||||||
|
|
||||||
namespace App\Http;
|
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\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
|
class Kernel extends HttpKernel
|
||||||
{
|
{
|
||||||
@@ -15,12 +42,12 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
// \App\Http\Middleware\TrustHosts::class,
|
// \App\Http\Middleware\TrustHosts::class,
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
TrustProxies::class,
|
||||||
\Illuminate\Http\Middleware\HandleCors::class,
|
HandleCors::class,
|
||||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
PreventRequestsDuringMaintenance::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
ValidatePostSize::class,
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
TrimStrings::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
ConvertEmptyStringsToNull::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,20 +57,20 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middlewareGroups = [
|
protected $middlewareGroups = [
|
||||||
'web' => [
|
'web' => [
|
||||||
\App\Http\Middleware\EncryptCookies::class,
|
EncryptCookies::class,
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
AddQueuedCookiesToResponse::class,
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
StartSession::class,
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
ShareErrorsFromSession::class,
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
\App\Http\Middleware\IsBanned::class,
|
IsBanned::class,
|
||||||
\App\Http\Middleware\SetLocale::class,
|
SetLocale::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'api' => [
|
'api' => [
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
ThrottleRequests::class.':api',
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
SubstituteBindings::class,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -55,18 +82,18 @@ class Kernel extends HttpKernel
|
|||||||
* @var array<string, class-string|string>
|
* @var array<string, class-string|string>
|
||||||
*/
|
*/
|
||||||
protected $middlewareAliases = [
|
protected $middlewareAliases = [
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
'auth' => Authenticate::class,
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
'auth.basic' => AuthenticateWithBasicAuth::class,
|
||||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
'auth.session' => AuthenticateSession::class,
|
||||||
'auth.admin' => \App\Http\Middleware\IsAdmin::class,
|
'auth.admin' => IsAdmin::class,
|
||||||
'auth.moderator' => \App\Http\Middleware\IsModerator::class,
|
'auth.moderator' => IsModerator::class,
|
||||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
'cache.headers' => SetCacheHeaders::class,
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
'can' => Authorize::class,
|
||||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
'guest' => RedirectIfAuthenticated::class,
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
'password.confirm' => RequirePassword::class,
|
||||||
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
|
'precognitive' => HandlePrecognitiveRequests::class,
|
||||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
'signed' => ValidateSignature::class,
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
'throttle' => ThrottleRequests::class,
|
||||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
'verified' => EnsureEmailIsVerified::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
<?php namespace app\Http\Middleware;
|
<?php
|
||||||
|
|
||||||
|
namespace app\Http\Middleware;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class IsAdmin {
|
class IsAdmin
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
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);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('error_msg', 'This resource is restricted to Administrators!');
|
session()->flash('error_msg', 'This resource is restricted to Administrators!');
|
||||||
|
|
||||||
return redirect()->route('home.index');
|
return redirect()->route('home.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
<?php namespace app\Http\Middleware;
|
<?php
|
||||||
|
|
||||||
|
namespace app\Http\Middleware;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
class IsBanned {
|
class IsBanned
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function handle(Request $request, Closure $next): Response
|
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();
|
Auth::logout();
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
$request->session()->regenerateToken();
|
$request->session()->regenerateToken();
|
||||||
|
|
||||||
return redirect()->route('home.banned');
|
return redirect()->route('home.banned');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@@ -14,16 +13,18 @@ class IsModerator
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* 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
|
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);
|
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');
|
return redirect()->route('home.index');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class RedirectIfAuthenticated
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* 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
|
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,19 +13,23 @@ class SetLocale
|
|||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* 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
|
public function handle(Request $request, Closure $next): Response
|
||||||
{
|
{
|
||||||
// 1. Logged-in user preference
|
// 1. Logged-in user preference
|
||||||
if (Auth::check() && Auth::user()->locale) {
|
if (Auth::check() && Auth::user()->locale) {
|
||||||
App::setLocale(Auth::user()->locale);
|
App::setLocale(Auth::user()->locale);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Session (guest or user override)
|
// 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'));
|
App::setLocale(session('locale'));
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Auth;
|
namespace App\Http\Requests\Auth;
|
||||||
|
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Auth\Events\Lockout;
|
use Illuminate\Auth\Events\Lockout;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
use GrantHolle\Altcha\Rules\ValidAltcha;
|
|
||||||
|
|
||||||
class LoginRequest extends FormRequest
|
class LoginRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -24,21 +24,21 @@ class LoginRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* 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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'string', 'email'],
|
'email' => ['required', 'string', 'email'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
'altcha' => ['required', new ValidAltcha],
|
'altcha' => ['required', new ValidCaptcha],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to authenticate the request's credentials.
|
* Attempt to authenticate the request's credentials.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function authenticate(): void
|
public function authenticate(): void
|
||||||
{
|
{
|
||||||
@@ -58,7 +58,7 @@ class LoginRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Ensure the login request is not rate limited.
|
* Ensure the login request is not rate limited.
|
||||||
*
|
*
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws ValidationException
|
||||||
*/
|
*/
|
||||||
public function ensureIsNotRateLimited(): void
|
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;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class ProfileUpdateRequest extends FormRequest
|
|||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
* 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
|
public function rules(): array
|
||||||
{
|
{
|
||||||
@@ -21,7 +22,7 @@ class ProfileUpdateRequest extends FormRequest
|
|||||||
'nullable',
|
'nullable',
|
||||||
'image',
|
'image',
|
||||||
'mimes:jpg,png,jpeg,webp,gif',
|
'mimes:jpg,png,jpeg,webp,gif',
|
||||||
'max:8192'
|
'max:8192',
|
||||||
],
|
],
|
||||||
'email' => [
|
'email' => [
|
||||||
'required',
|
'required',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
use Spatie\DiscordAlerts\Facades\DiscordAlert;
|
use Spatie\DiscordAlerts\Facades\DiscordAlert;
|
||||||
|
|
||||||
class DiscordReleaseNotification implements ShouldQueue
|
class DiscordReleaseNotification implements ShouldQueue
|
||||||
@@ -32,26 +31,30 @@ class DiscordReleaseNotification implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function handle(): void
|
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':
|
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;
|
break;
|
||||||
case 'release-censored':
|
case 'release-censored':
|
||||||
# Because Discord TOS
|
// Because Discord TOS
|
||||||
DiscordAlert::message("<@&868457842250764289> (´• ω •`)ノ New **4k** Release: ".$this->slug." - *No link here because of* :pLoli:");
|
DiscordAlert::message('<@&868457842250764289> (´• ω •`)ノ New **4k** Release: '.$this->slug.' - *No link here because of* :pLoli:');
|
||||||
break;
|
break;
|
||||||
case 'update':
|
case 'update':
|
||||||
# 1080p 48fps added
|
// 1080p 48fps added
|
||||||
DiscordAlert::to('update')->message("<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
|
DiscordAlert::to('update')->message('<@&1283518462584426598> (´• ω •`)ノ Added **48fps** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
|
||||||
break;
|
break;
|
||||||
case 'updateUHD':
|
case 'updateUHD':
|
||||||
# 4k 48fps added
|
// 4k 48fps added
|
||||||
DiscordAlert::to('update')->message("<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
|
DiscordAlert::to('update')->message('<@&1326860920902778963> (´• ω •`)ノ Added **48fps 4k** to Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
|
||||||
break;
|
break;
|
||||||
case 'v2':
|
case 'v2':
|
||||||
# v2 re-release
|
// v2 re-release
|
||||||
DiscordAlert::to('rerelease')->message("<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/".$this->slug);
|
DiscordAlert::to('rerelease')->message('<@&1425505303075754035> (´• ω •`)ノ **v2 Re-**Release! Check it out here: https://hstream.moe/hentai/'.$this->slug);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use Illuminate\Bus\Queueable;
|
|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Http\Client\RequestException;
|
use Illuminate\Http\Client\RequestException;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class GetFileSizeFromCDN implements ShouldQueue
|
class GetFileSizeFromCDN implements ShouldQueue
|
||||||
{
|
{
|
||||||
@@ -37,6 +37,7 @@ class GetFileSizeFromCDN implements ShouldQueue
|
|||||||
$download = Downloads::find($this->downloadId);
|
$download = Downloads::find($this->downloadId);
|
||||||
if (! $download) {
|
if (! $download) {
|
||||||
Log::error("Download not found for ID: {$this->downloadId}");
|
Log::error("Download not found for ID: {$this->downloadId}");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,46 +2,158 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
|
use App\Models\User;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class AdminCommentSearch extends Component
|
class AdminCommentSearch extends Component
|
||||||
{
|
{
|
||||||
use WithPagination;
|
use WithPagination;
|
||||||
|
|
||||||
|
#[Url(history: true)]
|
||||||
public $search = '';
|
public $search = '';
|
||||||
|
|
||||||
|
#[Url(history: true)]
|
||||||
public $userSearch = '';
|
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();
|
$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();
|
$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();
|
$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();
|
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()
|
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', [
|
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\Enums\UserRole;
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
|
|
||||||
class AdminUserSearch extends Component
|
class AdminUserSearch extends Component
|
||||||
{
|
{
|
||||||
@@ -21,32 +20,239 @@ class AdminUserSearch extends Component
|
|||||||
public $discordId = '';
|
public $discordId = '';
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $patreon = [];
|
public $email = '';
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[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)
|
$this->resetPage();
|
||||||
->firstOrFail();
|
}
|
||||||
|
|
||||||
Comment::where('user_id', $user->id)
|
public function updatedPage(): void
|
||||||
->delete();
|
{
|
||||||
|
$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();
|
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()
|
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', [
|
return view('livewire.admin-user-search', [
|
||||||
'users' => $users
|
'users' => $this->users,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use Livewire\Component;
|
|
||||||
use Livewire\WithPagination;
|
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
|
|
||||||
use App\Models\SiteBackground;
|
use App\Models\SiteBackground;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
class BackgroundImages extends Component
|
class BackgroundImages extends Component
|
||||||
{
|
{
|
||||||
@@ -20,16 +19,14 @@ class BackgroundImages extends Component
|
|||||||
{
|
{
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
|
|
||||||
$images = SiteBackground::when($this->filter === 'active', fn ($query) =>
|
$images = SiteBackground::when($this->filter === 'active', fn ($query) => $query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
|
||||||
$query->whereDate('date_start', '<=', $now)->whereDate('date_end', '>=', $now)
|
|
||||||
)
|
)
|
||||||
->when($this->filter === 'inactive', fn ($query) =>
|
->when($this->filter === 'inactive', fn ($query) => $query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
|
||||||
$query->whereDate('date_start', '>', $now)->orWhereDate('date_end', '<', $now)
|
|
||||||
)
|
)
|
||||||
->paginate(10);
|
->paginate(10);
|
||||||
|
|
||||||
return view('livewire.background-images', [
|
return view('livewire.background-images', [
|
||||||
'images' => $images
|
'images' => $images,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-17
@@ -1,20 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Enums\UserRole;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use App\Models\ModLog;
|
||||||
|
use App\Models\User;
|
||||||
use App\Notifications\CommentNotification;
|
use App\Notifications\CommentNotification;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Component;
|
||||||
use Maize\Markable\Models\Like;
|
use Maize\Markable\Models\Like;
|
||||||
|
|
||||||
class Comment extends Component
|
class Comment extends Component
|
||||||
@@ -30,31 +28,31 @@ class Comment extends Component
|
|||||||
public $liked = false;
|
public $liked = false;
|
||||||
|
|
||||||
public $replyState = [
|
public $replyState = [
|
||||||
'body' => ''
|
'body' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
public $isEditing = false;
|
public $isEditing = false;
|
||||||
|
|
||||||
public $editState = [
|
public $editState = [
|
||||||
'body' => ''
|
'body' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'refresh' => '$refresh'
|
'refresh' => '$refresh',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'replyState.body' => 'reply'
|
'replyState.body' => 'reply',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function updatedIsEditing($isEditing)
|
public function updatedIsEditing(bool $isEditing)
|
||||||
{
|
{
|
||||||
if (! $isEditing) {
|
if (! $isEditing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->editState = [
|
$this->editState = [
|
||||||
'body' => $this->comment->body
|
'body' => $this->comment->body,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +69,50 @@ class Comment extends Component
|
|||||||
{
|
{
|
||||||
$this->authorize('destroy', $this->comment);
|
$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->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()
|
public function postReply()
|
||||||
{
|
{
|
||||||
if (! ($this->comment->depth() < 2)) {
|
if (! ($this->comment->depth() < 2)) {
|
||||||
$this->addError('replyState.body', "Too many sub comments.");
|
$this->addError('replyState.body', 'Too many sub comments.');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,13 +124,14 @@ class Comment extends Component
|
|||||||
$seconds = RateLimiter::availableIn($rateLimitKey);
|
$seconds = RateLimiter::availableIn($rateLimitKey);
|
||||||
|
|
||||||
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
|
$this->addError('replyState.body', "Too many comments. Try again in {$seconds} seconds.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
|
RateLimiter::hit($rateLimitKey, $rateLimitMinutes);
|
||||||
|
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'replyState.body' => 'required'
|
'replyState.body' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$reply = $this->comment->children()->make($this->replyState);
|
$reply = $this->comment->children()->make($this->replyState);
|
||||||
@@ -123,7 +157,7 @@ class Comment extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->replyState = [
|
$this->replyState = [
|
||||||
'body' => ''
|
'body' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->isReplying = false;
|
$this->isReplying = false;
|
||||||
@@ -144,6 +178,7 @@ class Comment extends Component
|
|||||||
if ($this->liked) {
|
if ($this->liked) {
|
||||||
$this->liked = false;
|
$this->liked = false;
|
||||||
$this->likeCount--;
|
$this->likeCount--;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
|
||||||
|
|
||||||
class Comments extends Component
|
class Comments extends Component
|
||||||
{
|
{
|
||||||
use WithPagination;
|
use WithPagination;
|
||||||
@@ -14,21 +13,21 @@ class Comments extends Component
|
|||||||
public $model;
|
public $model;
|
||||||
|
|
||||||
public $newCommentState = [
|
public $newCommentState = [
|
||||||
'body' => ''
|
'body' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $validationAttributes = [
|
protected $validationAttributes = [
|
||||||
'newCommentState.body' => 'comment'
|
'newCommentState.body' => 'comment',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'refresh' => '$refresh'
|
'refresh' => '$refresh',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function postComment()
|
public function postComment()
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'newCommentState.body' => 'required'
|
'newCommentState.body' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@@ -39,6 +38,7 @@ class Comments extends Component
|
|||||||
$seconds = RateLimiter::availableIn($rateLimitKey);
|
$seconds = RateLimiter::availableIn($rateLimitKey);
|
||||||
|
|
||||||
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
|
$this->addError('newCommentState.body', "Too many comments. Try again in {$seconds} seconds.");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class Comments extends Component
|
|||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
$this->newCommentState = [
|
$this->newCommentState = [
|
||||||
'body' => ''
|
'body' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->resetPage();
|
$this->resetPage();
|
||||||
@@ -65,7 +65,7 @@ class Comments extends Component
|
|||||||
->paginate(50);
|
->paginate(50);
|
||||||
|
|
||||||
return view('livewire.comments', [
|
return view('livewire.comments', [
|
||||||
'comments' => $comments
|
'comments' => $comments,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,25 @@ class DownloadButton extends Component
|
|||||||
|
|
||||||
public $background = 'bg-rose-600';
|
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)
|
public function clicked($downloadId)
|
||||||
{
|
{
|
||||||
$download = Downloads::find($downloadId);
|
$download = Downloads::find($downloadId);
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ namespace App\Livewire;
|
|||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserDownload;
|
use App\Models\UserDownload;
|
||||||
|
|
||||||
use Livewire\Component;
|
|
||||||
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
class DownloadsFree extends Component
|
class DownloadsFree extends Component
|
||||||
{
|
{
|
||||||
@@ -51,6 +49,7 @@ class DownloadsFree extends Component
|
|||||||
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
|
if (Carbon::parse($alreadyDownloaded->created_at)->addHours(6) <= Carbon::now()) {
|
||||||
// Already expired
|
// Already expired
|
||||||
$alreadyDownloaded->delete();
|
$alreadyDownloaded->delete();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +64,7 @@ class DownloadsFree extends Component
|
|||||||
if ($user->downloads_left <= 0) {
|
if ($user->downloads_left <= 0) {
|
||||||
// Daily limit reached
|
// Daily limit reached
|
||||||
$this->granted = 3;
|
$this->granted = 3;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
use App\Models\Downloads;
|
use App\Models\Downloads;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
|
|
||||||
class DownloadsSearch extends Component
|
class DownloadsSearch extends Component
|
||||||
{
|
{
|
||||||
@@ -25,6 +26,7 @@ class DownloadsSearch extends Component
|
|||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $studios = [];
|
public $studios = [];
|
||||||
|
|
||||||
public $studiosCopy = [];
|
public $studiosCopy = [];
|
||||||
|
|
||||||
// To toggle individual option selection
|
// To toggle individual option selection
|
||||||
@@ -73,9 +75,9 @@ class DownloadsSearch extends Component
|
|||||||
$types[] = 'FHD';
|
$types[] = 'FHD';
|
||||||
} elseif ($label === 'FHD 48fps') {
|
} elseif ($label === 'FHD 48fps') {
|
||||||
$types[] = 'FHDi';
|
$types[] = 'FHDi';
|
||||||
} elseif ($label === 'UHD' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
} elseif ($label === 'UHD' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHD';
|
$types[] = 'UHD';
|
||||||
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
} elseif ($label === 'UHD 48fps' && auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
$types[] = 'UHDi';
|
$types[] = 'UHDi';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +100,7 @@ class DownloadsSearch extends Component
|
|||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (!auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER)) {
|
if (! auth()->user()->hasRole(UserRole::SUPPORTER)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +146,9 @@ class DownloadsSearch extends Component
|
|||||||
|
|
||||||
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
|
$downloads = Downloads::when($this->fileSearch != '', fn ($query) => $query->where('url', 'like', '%'.$this->fileSearch.'%'))
|
||||||
->whereIn('type', $this->getSelectedTypes())
|
->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')
|
->whereNotNull('size')
|
||||||
->orderBy($orderby, $orderdirection)
|
->orderBy($orderby, $orderdirection)
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ namespace App\Livewire;
|
|||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Livewire\Component;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
use Maize\Markable\Models\Like;
|
use Maize\Markable\Models\Like;
|
||||||
|
|
||||||
class LikeButton extends Component
|
class LikeButton extends Component
|
||||||
@@ -52,6 +51,7 @@ class LikeButton extends Component
|
|||||||
if ($this->liked) {
|
if ($this->liked) {
|
||||||
$this->liked = false;
|
$this->liked = false;
|
||||||
$this->likeCount--;
|
$this->likeCount--;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class LiveSearch extends Component
|
class LiveSearch extends Component
|
||||||
{
|
{
|
||||||
@@ -20,14 +20,17 @@ class LiveSearch extends Component
|
|||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $tags = [];
|
public $tags = [];
|
||||||
|
|
||||||
public $tagsCopy = [];
|
public $tagsCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $studios = [];
|
public $studios = [];
|
||||||
|
|
||||||
public $studiosCopy = [];
|
public $studiosCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $blacklist = [];
|
public $blacklist = [];
|
||||||
|
|
||||||
public $blacklistCopy = [];
|
public $blacklistCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
@@ -118,10 +121,14 @@ class LiveSearch extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user_id = Auth::check() ? auth()->user()->id : 0;
|
$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.'%'); }))
|
$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->tags !== [], fn ($query) => $query->withAllTags($this->tags))
|
||||||
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
|
->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->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->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
|
||||||
$query->where('user_id', $user_id);
|
$query->where('user_id', $user_id);
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use Livewire\Component;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
class NavLiveSearch extends Component
|
class NavLiveSearch extends Component
|
||||||
{
|
{
|
||||||
@@ -20,6 +20,7 @@ class NavLiveSearch extends Component
|
|||||||
if ($this->navSearch != '') {
|
if ($this->navSearch != '') {
|
||||||
$episodes = Episode::search($this->navSearch)
|
$episodes = Episode::search($this->navSearch)
|
||||||
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
|
->when(Auth::guest(), fn ($query) => $query->whereNotIn('tags', ['Loli', 'Shota']))
|
||||||
|
->query(fn ($query) => $query->with(['gallery', 'studio']))
|
||||||
->take(7)
|
->take(7)
|
||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\Episode;
|
||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
use App\Models\PlaylistEpisode;
|
use App\Models\PlaylistEpisode;
|
||||||
use App\Services\PlaylistService;
|
use App\Services\PlaylistService;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
|
|
||||||
class PlaylistOverview extends Component
|
class PlaylistOverview extends Component
|
||||||
{
|
{
|
||||||
@@ -20,13 +19,16 @@ class PlaylistOverview extends Component
|
|||||||
protected PlaylistService $playlistService;
|
protected PlaylistService $playlistService;
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $search;
|
public string $search = '';
|
||||||
|
|
||||||
public int $pagination = 25;
|
#[Url(history: true)]
|
||||||
|
public int $perPage = 25;
|
||||||
|
|
||||||
public Playlist $playlist;
|
public Playlist $playlist;
|
||||||
|
|
||||||
public Collection $playlistEpisodes;
|
public bool $editingName = false;
|
||||||
|
|
||||||
|
public string $editingPlaylistName = '';
|
||||||
|
|
||||||
public function boot(PlaylistService $playlistService)
|
public function boot(PlaylistService $playlistService)
|
||||||
{
|
{
|
||||||
@@ -35,35 +37,56 @@ class PlaylistOverview extends Component
|
|||||||
|
|
||||||
public function mount($playlist_id)
|
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->sanitizePerPage();
|
||||||
$this->playlist->episodes->each(function ($item, $index) {
|
|
||||||
if ($item->position === null) {
|
|
||||||
$item->position = $index + 1;
|
|
||||||
$item->save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$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)
|
public function moveUp($episodeId)
|
||||||
{
|
{
|
||||||
if (! Auth::check()) {
|
if (! $this->isOwner()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$episode = PlaylistEpisode::find($episodeId);
|
$episode = PlaylistEpisode::find($episodeId);
|
||||||
|
|
||||||
|
if (! $episode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$above = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
$above = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
||||||
->where('position', '<', $episode->position)
|
->where('position', '<', $episode->position)
|
||||||
->orderBy('position', 'desc')
|
->orderBy('position', 'desc')
|
||||||
@@ -72,21 +95,20 @@ class PlaylistOverview extends Component
|
|||||||
if ($above) {
|
if ($above) {
|
||||||
$this->playlistService->swapPositions($episode, $above);
|
$this->playlistService->swapPositions($episode, $above);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->refreshEpisodes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function moveDown($episodeId)
|
public function moveDown($episodeId)
|
||||||
{
|
{
|
||||||
if (! Auth::check()) {
|
if (! $this->isOwner()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$episode = PlaylistEpisode::find($episodeId);
|
$episode = PlaylistEpisode::find($episodeId);
|
||||||
|
|
||||||
|
if (! $episode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$below = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
$below = PlaylistEpisode::where('playlist_id', $episode->playlist_id)
|
||||||
->where('position', '>', $episode->position)
|
->where('position', '>', $episode->position)
|
||||||
->orderBy('position')
|
->orderBy('position')
|
||||||
@@ -95,29 +117,102 @@ class PlaylistOverview extends Component
|
|||||||
if ($below) {
|
if ($below) {
|
||||||
$this->playlistService->swapPositions($episode, $below);
|
$this->playlistService->swapPositions($episode, $below);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->refreshEpisodes();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remove($episodeId)
|
public function remove($episodeId)
|
||||||
{
|
{
|
||||||
if (! Auth::check()) {
|
if (! $this->isOwner()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Auth::user()->id !== $this->playlist->user->id) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistEpisode::find($episodeId)?->delete();
|
PlaylistEpisode::find($episodeId)?->delete();
|
||||||
$this->playlistService->reorderPositions($this->playlist);
|
$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()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.playlist-overview', [
|
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;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Playlist;
|
use App\Models\Playlist;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
|
|
||||||
class Playlists extends Component
|
class Playlists extends Component
|
||||||
{
|
{
|
||||||
@@ -55,11 +55,30 @@ class Playlists extends Component
|
|||||||
->withCount('episodes')
|
->withCount('episodes')
|
||||||
->having('episodes_count', '>', 1)
|
->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)
|
->orderBy($orderby, $orderdirection)
|
||||||
->paginate($this->pagination);
|
->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', [
|
return view('livewire.playlists', [
|
||||||
'playlists' => $playlists
|
'playlists' => $playlists,
|
||||||
|
'totalPlaylists' => $stats['playlists'],
|
||||||
|
'totalEpisodes' => $stats['episodes'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
|
|
||||||
@@ -36,14 +35,13 @@ class UserComments extends Component
|
|||||||
$orderdirection = 'desc';
|
$orderdirection = 'desc';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$comments = Comment::where('user_id', $this->model->id)
|
$comments = Comment::where('user_id', $this->model->id)
|
||||||
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
|
->when($this->commentSearch != '', fn ($query) => $query->where('body', 'like', '%'.$this->commentSearch.'%'))
|
||||||
->orderBy($orderby, $orderdirection)
|
->orderBy($orderby, $orderdirection)
|
||||||
->paginate(10);
|
->paginate(6);
|
||||||
|
|
||||||
return view('livewire.user-comments', [
|
return view('livewire.user-comments', [
|
||||||
'comments' => $comments
|
'comments' => $comments,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
use Livewire\Attributes\Url;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
|
|
||||||
class UserLikes extends Component
|
class UserLikes extends Component
|
||||||
{
|
{
|
||||||
@@ -20,14 +20,17 @@ class UserLikes extends Component
|
|||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $tags = [];
|
public $tags = [];
|
||||||
|
|
||||||
public $tagsCopy = [];
|
public $tagsCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $studios = [];
|
public $studios = [];
|
||||||
|
|
||||||
public $studiosCopy = [];
|
public $studiosCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
public $blacklist = [];
|
public $blacklist = [];
|
||||||
|
|
||||||
public $blacklistCopy = [];
|
public $blacklistCopy = [];
|
||||||
|
|
||||||
#[Url(history: true)]
|
#[Url(history: true)]
|
||||||
@@ -119,10 +122,14 @@ class UserLikes extends Component
|
|||||||
|
|
||||||
$user_id = Auth::check() ? auth()->user()->id : 0;
|
$user_id = Auth::check() ? auth()->user()->id : 0;
|
||||||
$episodes = Episode::whereHasLike(auth()->user())
|
$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->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->tags !== [], fn ($query) => $query->withAllTags($this->tags))
|
||||||
->when($this->blacklist !== [], fn ($query) => $query->withoutTags($this->blacklist))
|
->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->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->hideWatched !== [] && Auth::check(), fn ($query) => $query->whereDoesntHave('watched', function ($query) use ($user_id) {
|
||||||
$query->where('user_id', $user_id);
|
$query->where('user_id', $user_id);
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -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;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Models\Watched as UserWatched;
|
use App\Models\Watched as UserWatched;
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\WithPagination;
|
use Livewire\WithPagination;
|
||||||
|
|||||||
+13
-6
@@ -4,19 +4,18 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Models\Presenters\CommentPresenter;
|
use App\Models\Presenters\CommentPresenter;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
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\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Maize\Markable\Markable;
|
use Maize\Markable\Markable;
|
||||||
use Maize\Markable\Models\Like;
|
use Maize\Markable\Models\Like;
|
||||||
|
|
||||||
class Comment extends Model
|
class Comment extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, SoftDeletes, Markable;
|
use HasFactory, Markable, SoftDeletes;
|
||||||
|
|
||||||
protected static $marks = [
|
protected static $marks = [
|
||||||
Like::class
|
Like::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +24,7 @@ class Comment extends Model
|
|||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'body'
|
'body',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function presenter()
|
public function presenter()
|
||||||
@@ -73,4 +72,12 @@ class Comment extends Model
|
|||||||
{
|
{
|
||||||
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',
|
'episode_id',
|
||||||
'type',
|
'type',
|
||||||
'url',
|
'url',
|
||||||
|
'size',
|
||||||
|
'validated_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+13
-18
@@ -2,35 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Downloads;
|
|
||||||
use App\Models\PopularMonthly;
|
|
||||||
use App\Models\PopularWeekly;
|
|
||||||
use App\Models\PopularDaily;
|
|
||||||
|
|
||||||
use Conner\Tagging\Taggable;
|
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\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
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
|
class Episode extends Model implements Sitemapable
|
||||||
{
|
{
|
||||||
use Markable, Taggable;
|
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use Markable, Taggable;
|
||||||
use Searchable;
|
use Searchable;
|
||||||
|
|
||||||
protected static $marks = [
|
protected static $marks = [
|
||||||
Like::class
|
Like::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -180,6 +173,7 @@ class Episode extends Model implements Sitemapable
|
|||||||
|
|
||||||
$problematicResults .= $pTag;
|
$problematicResults .= $pTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $problematicResults;
|
return $problematicResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,9 +228,10 @@ class Episode extends Model implements Sitemapable
|
|||||||
return $this->hasMany(Watched::class);
|
return $this->hasMany(Watched::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDownloadByType(string $type): Downloads | null
|
public function getDownloadByType(string $type): ?Downloads
|
||||||
{
|
{
|
||||||
$cacheKey = "episode_{$this->id}_download_{$type}";
|
$cacheKey = "episode_{$this->id}_download_{$type}";
|
||||||
|
|
||||||
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) {
|
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($type) {
|
||||||
return $this->downloads()->where('type', $type)->first();
|
return $this->downloads()->where('type', $type)->first();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,22 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Gallery extends Model
|
class Gallery extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
public $table = 'gallery';
|
public $table = 'gallery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that aren't mass assignable.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Belongs To Episode.
|
* Belongs To Episode.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,19 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
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\Contracts\Sitemapable;
|
||||||
use Spatie\Sitemap\Tags\Url;
|
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
|
class Hentai extends Model implements Sitemapable
|
||||||
{
|
{
|
||||||
use Taggable;
|
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use Taggable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
@@ -31,7 +30,7 @@ class Hentai extends Model implements Sitemapable
|
|||||||
return $this->hasMany(Episode::class, 'hentai_id');
|
return $this->hasMany(Episode::class, 'hentai_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function title(): String
|
public function title(): string
|
||||||
{
|
{
|
||||||
return $this->episodes->first()->title;
|
return $this->episodes->first()->title;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Playlist extends Model
|
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.
|
* Belongs To A User.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class PlaylistEpisode extends Model
|
class PlaylistEpisode extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates If The Model Should Be Timestamped.
|
* Indicates If The Model Should Be Timestamped.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ class SiteBackground extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'date_start',
|
'date_start',
|
||||||
'date_end',
|
'date_end',
|
||||||
'default'
|
'default',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current IDs of active wallpaper
|
* Returns the current IDs of active wallpaper
|
||||||
*/
|
*/
|
||||||
public function getImages(): ? \Illuminate\Support\Collection
|
public function getImages(): ?Collection
|
||||||
{
|
{
|
||||||
$now = Carbon::now();
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
class Studios extends Model
|
class Studios extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
+13
-11
@@ -5,17 +5,18 @@ namespace App\Models;
|
|||||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
|
||||||
use App\Enums\UserRole;
|
use App\Enums\UserRole;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
|
|
||||||
use Illuminate\Support\Facades\DB;
|
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.
|
* The attributes that are mass assignable.
|
||||||
@@ -62,7 +63,6 @@ class User extends Authenticatable
|
|||||||
'discord_avatar' => 'string',
|
'discord_avatar' => 'string',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has Many Playlists.
|
* Has Many Playlists.
|
||||||
*/
|
*/
|
||||||
@@ -108,13 +108,11 @@ class User extends Authenticatable
|
|||||||
*/
|
*/
|
||||||
public function getAvatar(): string
|
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}";
|
return "https://external-content.duckduckgo.com/iu/?u={$this->discord_avatar}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->avatar)
|
if ($this->avatar) {
|
||||||
{
|
|
||||||
return Storage::url($this->avatar);
|
return Storage::url($this->avatar);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +155,11 @@ class User extends Authenticatable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->roles = array_diff($this->roles, [$role->value]);
|
$this->roles = collect($this->roles)
|
||||||
|
->reject(fn ($value) => $value === $role->value)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class UserDownload extends Model
|
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;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class Watched extends Model
|
class Watched extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
namespace App\Notifications;
|
namespace App\Notifications;
|
||||||
|
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
class CommentNotification extends Notification
|
class CommentNotification extends Notification
|
||||||
@@ -12,7 +10,9 @@ class CommentNotification extends Notification
|
|||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
protected $type;
|
protected $type;
|
||||||
|
|
||||||
protected $message;
|
protected $message;
|
||||||
|
|
||||||
protected $url;
|
protected $url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Policies;
|
namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\User;
|
use App\Enums\UserRole;
|
||||||
use App\Models\Comment;
|
use App\Models\Comment;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
class CommentPolicy
|
class CommentPolicy
|
||||||
@@ -17,6 +18,26 @@ class CommentPolicy
|
|||||||
|
|
||||||
public function destroy(User $user, Comment $comment): bool
|
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;
|
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\Facades\Event;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use SocialiteProviders\Discord\Provider;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -20,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
|
Event::listen(function (SocialiteWasCalled $event) {
|
||||||
$event->extendSocialite('discord', \SocialiteProviders\Discord\Provider::class);
|
$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;
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Jobs\GetFileSizeFromCDN;
|
||||||
use App\Models\Downloads;
|
use App\Models\Downloads;
|
||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
|
|
||||||
use App\Jobs\GetFileSizeFromCDN;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class DownloadService
|
class DownloadService
|
||||||
@@ -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\Episode;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
|
use App\Models\ModLog;
|
||||||
use App\Models\Studios;
|
use App\Models\Studios;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Intervention\Image\Laravel\Facades\Image;
|
|
||||||
use Intervention\Image\Encoders\WebpEncoder;
|
use Intervention\Image\Encoders\WebpEncoder;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
|
|
||||||
class EpisodeService
|
class EpisodeService
|
||||||
{
|
{
|
||||||
@@ -24,44 +25,73 @@ class EpisodeService
|
|||||||
|
|
||||||
if (is_numeric($lastPart) && $lastPart < 1000) {
|
if (is_numeric($lastPart) && $lastPart < 1000) {
|
||||||
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
|
$slugParts[array_key_last($slugParts)] = 's'.$lastPart;
|
||||||
|
|
||||||
return implode('-', $slugParts);
|
return implode('-', $slugParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $slug;
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createEpisode(
|
private function applyTags(Request $request, Episode $episode): void
|
||||||
Request $request,
|
|
||||||
Hentai $hentai,
|
|
||||||
int $episodeNumber,
|
|
||||||
?Studios $studio = null,
|
|
||||||
?Episode $referenceEpisode = null
|
|
||||||
): Episode
|
|
||||||
{
|
{
|
||||||
$episode = new Episode();
|
$tags = json_decode($request->input('tags'));
|
||||||
$episode->title = $referenceEpisode->title ?? $request->input('title');
|
$newtags = [];
|
||||||
$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'));
|
|
||||||
foreach ($tags as $t) {
|
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
|
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->dmca_takedown = $request->input('dmca_takedown') == 'true';
|
||||||
$episode->save();
|
$episode->save();
|
||||||
|
|
||||||
// Tagging
|
$this->applyTags($request, $episode);
|
||||||
$tags = json_decode($request->input('tags'));
|
$this->updateTitle($request, $episode);
|
||||||
$newtags = [];
|
|
||||||
foreach ($tags as $t) {
|
|
||||||
$newtags[] = $t->value;
|
|
||||||
}
|
|
||||||
$episode->retag($newtags);
|
|
||||||
|
|
||||||
return $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
|
public function getOrCreateStudio(string $studioName): Studios
|
||||||
{
|
{
|
||||||
return Studios::firstOrCreate(
|
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
|
public function createOrUpdateCover(Request $request, Episode $episode, string $slug, int $episodeNumber): void
|
||||||
{
|
{
|
||||||
@@ -103,15 +182,20 @@ class EpisodeService
|
|||||||
return;
|
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
|
// Create Folder for Image Upload
|
||||||
if (! Storage::disk('public')->exists("/images/hentai/{$slug}")) {
|
if (! Storage::disk('public')->exists("/images/hentai/{$slug}")) {
|
||||||
Storage::disk('public')->makeDirectory("/images/hentai/{$slug}");
|
Storage::disk('public')->makeDirectory("/images/hentai/{$slug}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode and save cover image
|
// Encode and save cover image
|
||||||
Image::read($request->file("episodecover{$episodeNumber}")->getRealPath())
|
Image::read($file->getRealPath())
|
||||||
->cover(268, 394)
|
->cover(268, 394)
|
||||||
->encode(new WebpEncoder())
|
->encode(new WebpEncoder)
|
||||||
->save(Storage::disk('public')->path($episode->cover_url));
|
->save(Storage::disk('public')->path($episode->cover_url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,35 +5,42 @@ namespace App\Services;
|
|||||||
use App\Models\Episode;
|
use App\Models\Episode;
|
||||||
use App\Models\Gallery;
|
use App\Models\Gallery;
|
||||||
use App\Models\Hentai;
|
use App\Models\Hentai;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
use Intervention\Image\Laravel\Facades\Image;
|
|
||||||
use Intervention\Image\Encoders\WebpEncoder;
|
use Intervention\Image\Encoders\WebpEncoder;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image;
|
||||||
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
|
|
||||||
class GalleryService
|
class GalleryService
|
||||||
{
|
{
|
||||||
|
|
||||||
public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void
|
public function createOrUpdateGallery(Request $request, Hentai $hentai, Episode $episode, int $episodeNumber, bool $override = false): void
|
||||||
{
|
{
|
||||||
$galleryInputNumber = $override ? 1 : $episodeNumber;
|
$galleryInputNumber = $override ? 1 : $episodeNumber;
|
||||||
|
|
||||||
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
|
if ($request->hasFile('episodegallery'.$galleryInputNumber)) {
|
||||||
|
$this->saveGalleryFiles($hentai, $episode, $episodeNumber, $request->file('episodegallery'.$galleryInputNumber));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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->deleteOldGallery($episode);
|
||||||
|
|
||||||
$this->createGalleryFolder($hentai);
|
$this->createGalleryFolder($hentai);
|
||||||
|
|
||||||
$counter = 0;
|
$counter = 0;
|
||||||
foreach($request->file('episodegallery'.$galleryInputNumber) as $file) {
|
foreach ($files as $file) {
|
||||||
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
$gallery = $this->createGallery($hentai, $episode, $episodeNumber, $counter);
|
||||||
$this->saveGalleryImage($gallery, $file);
|
$this->saveGalleryImage($gallery, $file);
|
||||||
$counter += 1;
|
$counter += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private function createGalleryFolder(Hentai $hentai): void
|
private function createGalleryFolder(Hentai $hentai): void
|
||||||
{
|
{
|
||||||
@@ -45,7 +52,7 @@ class GalleryService
|
|||||||
|
|
||||||
private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery
|
private function createGallery(Hentai $hentai, Episode $episode, int $episodeNumber, int $counter): Gallery
|
||||||
{
|
{
|
||||||
$gallery = new Gallery();
|
$gallery = new Gallery;
|
||||||
$gallery->hentai_id = $hentai->id;
|
$gallery->hentai_id = $hentai->id;
|
||||||
$gallery->episode_id = $episode->id;
|
$gallery->episode_id = $episode->id;
|
||||||
$gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp";
|
$gallery->image_url = "/images/hentai/{$hentai->slug}/gallery-ep-{$episodeNumber}-{$counter}.webp";
|
||||||
@@ -55,16 +62,16 @@ class GalleryService
|
|||||||
return $gallery;
|
return $gallery;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveGalleryImage(Gallery $gallery, $sourceImage): void
|
private function saveGalleryImage(Gallery $gallery, UploadedFile|TemporaryUploadedFile $sourceImage): void
|
||||||
{
|
{
|
||||||
Image::read($sourceImage->getRealPath())
|
Image::read($sourceImage->getRealPath())
|
||||||
->cover(1920, 1080)
|
->cover(1920, 1080)
|
||||||
->encode(new WebpEncoder())
|
->encode(new WebpEncoder)
|
||||||
->save(Storage::disk('public')->path($gallery->image_url));
|
->save(Storage::disk('public')->path($gallery->image_url));
|
||||||
|
|
||||||
Image::read($sourceImage->getRealPath())
|
Image::read($sourceImage->getRealPath())
|
||||||
->cover(960, 540)
|
->cover(960, 540)
|
||||||
->encode(new WebpEncoder())
|
->encode(new WebpEncoder)
|
||||||
->save(Storage::disk('public')->path($gallery->thumbnail_url));
|
->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
|
<?php
|
||||||
|
|
||||||
|
use App\Exceptions\Handler;
|
||||||
|
use App\Http\Kernel;
|
||||||
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Create The Application
|
| Create The Application
|
||||||
@@ -11,7 +16,7 @@
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$app = new Illuminate\Foundation\Application(
|
$app = new Application(
|
||||||
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ $app = new Illuminate\Foundation\Application(
|
|||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
Illuminate\Contracts\Http\Kernel::class,
|
Illuminate\Contracts\Http\Kernel::class,
|
||||||
App\Http\Kernel::class
|
Kernel::class
|
||||||
);
|
);
|
||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
@@ -37,8 +42,8 @@ $app->singleton(
|
|||||||
);
|
);
|
||||||
|
|
||||||
$app->singleton(
|
$app->singleton(
|
||||||
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
ExceptionHandler::class,
|
||||||
App\Exceptions\Handler::class
|
Handler::class
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
+3
-1
@@ -9,7 +9,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"grantholle/laravel-altcha": "^2.1",
|
"altcha-org/altcha": "^2.0",
|
||||||
"guzzlehttp/guzzle": "^7.8.1",
|
"guzzlehttp/guzzle": "^7.8.1",
|
||||||
"hisorange/browser-detect": "^5.0",
|
"hisorange/browser-detect": "^5.0",
|
||||||
"http-interop/http-factory-guzzle": "^1.2",
|
"http-interop/http-factory-guzzle": "^1.2",
|
||||||
@@ -28,11 +28,13 @@
|
|||||||
"rtconner/laravel-tagging": "^5.0",
|
"rtconner/laravel-tagging": "^5.0",
|
||||||
"socialiteproviders/discord": "^4.2",
|
"socialiteproviders/discord": "^4.2",
|
||||||
"spatie/laravel-discord-alerts": "^1.8",
|
"spatie/laravel-discord-alerts": "^1.8",
|
||||||
|
"spatie/laravel-passkeys": "^1.7",
|
||||||
"spatie/laravel-sitemap": "^7.3"
|
"spatie/laravel-sitemap": "^7.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.16",
|
"barryvdh/laravel-debugbar": "^3.16",
|
||||||
"fakerphp/faker": "^1.24.0",
|
"fakerphp/faker": "^1.24.0",
|
||||||
|
"laravel/boost": "^2.4",
|
||||||
"laravel/breeze": "^2.3",
|
"laravel/breeze": "^2.3",
|
||||||
"laravel/pint": "^1.18",
|
"laravel/pint": "^1.18",
|
||||||
"mockery/mockery": "^1.4.4",
|
"mockery/mockery": "^1.4.4",
|
||||||
|
|||||||
Generated
+2088
-757
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -1,7 +1,12 @@
|
|||||||
<?php
|
<?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\Facades\Facade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Mews\Captcha\Facades\Captcha;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -175,11 +180,11 @@ return [
|
|||||||
/*
|
/*
|
||||||
* Application Service Providers...
|
* Application Service Providers...
|
||||||
*/
|
*/
|
||||||
App\Providers\AppServiceProvider::class,
|
AppServiceProvider::class,
|
||||||
App\Providers\AuthServiceProvider::class,
|
AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
// App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
EventServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
RouteServiceProvider::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -195,7 +200,7 @@ return [
|
|||||||
|
|
||||||
'aliases' => Facade::defaultAliases()->merge([
|
'aliases' => Facade::defaultAliases()->merge([
|
||||||
// 'Example' => App\Facades\Example::class,
|
// 'Example' => App\Facades\Example::class,
|
||||||
'Captcha' => Mews\Captcha\Facades\Captcha::class,
|
'Captcha' => Captcha::class,
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -62,7 +64,7 @@ return [
|
|||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\User::class,
|
'model' => User::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Altcha Captcha System
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'hmac_key' => env('ALTCHA_HMAC_KEY'),
|
||||||
|
];
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
/*
|
||||||
* The webhook URLs that we'll use to send a message to Discord.
|
* The webhook URLs that we'll use to send a message to Discord.
|
||||||
@@ -14,5 +16,5 @@ return [
|
|||||||
* This job will send the message to Discord. You can extend this
|
* This job will send the message to Discord. You can extend this
|
||||||
* job to set timeouts, retries, etc...
|
* job to set timeouts, retries, etc...
|
||||||
*/
|
*/
|
||||||
'job' => Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob::class,
|
'job' => SendToDiscordChannelJob::class,
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ return [
|
|||||||
'803325441942356059', // Tier-3
|
'803325441942356059', // Tier-3
|
||||||
'803322725576736858', // Tier-2
|
'803322725576736858', // Tier-2
|
||||||
'802270568912519198', // Tier-1
|
'802270568912519198', // Tier-1
|
||||||
'802234830384267315' // admin
|
'802234830384267315', // admin
|
||||||
],
|
],
|
||||||
|
|
||||||
'discord_bot_token' => env('DISCORD_BOT_TOKEN'),
|
'discord_bot_token' => env('DISCORD_BOT_TOKEN'),
|
||||||
|
|||||||
+2
-2
@@ -30,11 +30,11 @@ return [
|
|||||||
|
|
||||||
// Asia Fallback (HTTP)
|
// Asia Fallback (HTTP)
|
||||||
'asia_download_domain' => [
|
'asia_download_domain' => [
|
||||||
'https://komako-b-ddl.musume-h.xyz'
|
'https://komako-b-ddl.musume-h.xyz',
|
||||||
],
|
],
|
||||||
|
|
||||||
'asia_stream_domain' => [
|
'asia_stream_domain' => [
|
||||||
'https://komako-b-str.musume-h.xyz'
|
'https://komako-b-str.musume-h.xyz',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Free 4k Downloads
|
// Free 4k Downloads
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user