Add (ai gen)
This commit is contained in:
@@ -72,3 +72,6 @@ DEV_DEFAULT_LOCATION=MY
|
||||
DEV_DEFAULT_IP=202.188.193.93
|
||||
|
||||
INDEXNOW_KEY=xxxxxxxx-xxxx-xxxxx-xxxx-xxxxxxxxxx
|
||||
|
||||
NODE_BINARY=/Users/xxx/.nvm/versions/node/v19.3.0/bin/node
|
||||
NPM_BINARY=/Users/xxx/.nvm/versions/node/v19.3.0/bin/npm
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
@@ -20,11 +22,44 @@ class Handler extends ExceptionHandler
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void
|
||||
public function register()
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
$this->reportable(function (Throwable $exception) {
|
||||
//
|
||||
|
||||
});
|
||||
|
||||
$this->renderable(function (NotFoundHttpException $e, $request) {
|
||||
if ($request->is('api/*')) {
|
||||
return response()->json([
|
||||
'status' => -1,
|
||||
], 404);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function render($request, Throwable $exception)
|
||||
{
|
||||
|
||||
if ($exception instanceof NotFoundHttpException) {
|
||||
|
||||
} elseif ($exception instanceof AuthenticationException) {
|
||||
|
||||
} else {
|
||||
inspector()->reportException($exception);
|
||||
}
|
||||
|
||||
//default laravel response
|
||||
return parent::render($request, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
67
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
67
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OSSUploader;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class OSSUploader
|
||||
{
|
||||
public static function readJson($storage_driver, $relative_directory, $filename)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
try {
|
||||
$jsonContent = Storage::disk($storage_driver)->get($filepath);
|
||||
|
||||
$decodedJson = json_decode($jsonContent, false, 512);
|
||||
|
||||
return $decodedJson;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function readFile($storage_driver, $relative_directory, $filename)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
try {
|
||||
return Storage::disk($storage_driver)->get($filepath);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function uploadJson($storage_driver, $relative_directory, $filename, $jsonData)
|
||||
{
|
||||
$jsonString = json_encode($jsonData, JSON_PRETTY_PRINT);
|
||||
|
||||
try {
|
||||
return self::uploadFile($storage_driver, $relative_directory, $filename, $jsonString);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public static function uploadFile($storage_driver, $relative_directory, $filename, $file)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
// if(!Storage::disk($storage_driver)->exists($relative_directory))
|
||||
// {
|
||||
// Storage::disk($storage_driver)->makeDirectory($relative_directory);
|
||||
// }
|
||||
|
||||
return Storage::disk($storage_driver)->put($filepath, $file);
|
||||
}
|
||||
}
|
||||
83
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
83
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OpenAI;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class OpenAI
|
||||
{
|
||||
public static function writeProductArticle($excerpt, $photos)
|
||||
{
|
||||
$system_prompt = '
|
||||
You are tasked with writing a comprehensive product introduction article using the provided excerpt. The emphasis should be on the performance, features, and notable aspects of the product. The review should avoid the use of personal pronouns and must not delve into marketplace-related information. Return the output in the following json format:\n\n
|
||||
{"title": "(Article Title)","excerpt": "(One sentence summary, 150-160 characters of an article, do not use start sentence with verb.)","cliffhanger": "(One sentence 70-80 characters of article, cliff-hanging sentence to attract readers)","body": "(Markdown format, 500-700 word count)"}\n\n
|
||||
Mandatory Requirements:\n
|
||||
- Write in US grade 8-9 English\n
|
||||
- Use the following sections whenever applicable:\n
|
||||
-- ### Introduction\n
|
||||
-- ### Overview\n
|
||||
-- ### Specifications (use valid Markdown table format with header and seperator when possible) and explanation\n
|
||||
-- ### Price\n
|
||||
-- ### Should I Buy?\n
|
||||
- do not make up facts, use facts provided by excerpt only\n
|
||||
- No article titles inside markdown\n
|
||||
- All article sections use ###
|
||||
- Add at least 3 markdown images with article title as caption in every section except for Introduction
|
||||
';
|
||||
|
||||
$user_prompt = "Excerpt: {$excerpt}\nPhotos:\n";
|
||||
|
||||
foreach ($photos as $photo) {
|
||||
$user_prompt .= "{$photo}\n";
|
||||
}
|
||||
|
||||
$output = (self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo', 2000));
|
||||
|
||||
if (! is_null($output)) {
|
||||
try {
|
||||
return json_decode($output, false, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception $e) {
|
||||
Log::error($output);
|
||||
inspector()->reportException($e);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
public static function chatCompletion($system_prompt, $user_prompt, $model, $max_token = 2500)
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(800)->withToken(config('platform.ai.openai.api_key'))
|
||||
->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => $model,
|
||||
'max_tokens' => $max_token,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $system_prompt],
|
||||
['role' => 'user', 'content' => $user_prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
//dd($response->body());
|
||||
|
||||
$json_response = json_decode($response->body(), false, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$reply = $json_response?->choices[0]?->message?->content;
|
||||
|
||||
return $reply;
|
||||
} catch (Exception $e) {
|
||||
Log::error($response->body());
|
||||
inspector()->reportException($e);
|
||||
throw ($e);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
require 'string_helper.php';
|
||||
require 'geo_helper.php';
|
||||
require 'proxy_helper.php';
|
||||
|
||||
33
app/Helpers/Global/proxy_helper.php
Normal file
33
app/Helpers/Global/proxy_helper.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('get_smartproxy_server')) {
|
||||
function get_smartproxy_server()
|
||||
{
|
||||
$proxy = config('platform.proxy.smartproxy.rotating_global.server');
|
||||
$proxy_user = config('platform.proxy.smartproxy.rotating_global.user');
|
||||
$proxy_psw = config('platform.proxy.smartproxy.rotating_global.password');
|
||||
|
||||
$reproxy_enable = config('platform.proxy.smartproxy.rotating_global.reproxy_enable');
|
||||
if ($reproxy_enable) {
|
||||
$proxy = config('platform.proxy.smartproxy.rotating_global.reproxy');
|
||||
}
|
||||
$proxy_server = "$proxy_user:$proxy_psw@$proxy";
|
||||
|
||||
return $proxy_server;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('calculate_smartproxy_cost')) {
|
||||
function calculate_smartproxy_cost(float $kb, $amplifier = 1)
|
||||
{
|
||||
$cost_per_gb = config('platform.proxy.smartproxy.rotating_global.cost_per_gb');
|
||||
|
||||
// Convert cost per GB to cost per KB
|
||||
$cost_per_kb = $cost_per_gb / (1024 * 1024);
|
||||
|
||||
// Calculate total cost for the given $kb
|
||||
$cost = ($cost_per_kb * $kb) * $amplifier;
|
||||
|
||||
return round($cost, 3);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (! function_exists('epoch_now_timestamp')) {
|
||||
function epoch_now_timestamp()
|
||||
{
|
||||
return (int) round(microtime(true) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_first_sentence')) {
|
||||
function str_first_sentence($str)
|
||||
{
|
||||
@@ -19,6 +26,20 @@ function str_first_sentence($str)
|
||||
|
||||
}
|
||||
|
||||
if (! function_exists('unslug')) {
|
||||
function unslug($slug, $delimiter = '-')
|
||||
{
|
||||
return ucwords(str_replace($delimiter, ' ', $slug));
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_slug')) {
|
||||
function str_slug($string, $delimiter = '-')
|
||||
{
|
||||
return Str::of(trim($string))->slug($delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('is_empty')) {
|
||||
/**
|
||||
* A better function to check if a value is empty or null. Strings, arrays, and Objects are supported.
|
||||
|
||||
31
app/Http/Controllers/Tests/ScraperTestController.php
Normal file
31
app/Http/Controllers/Tests/ScraperTestController.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Tests;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ShopeeSellerTopProductScraperJob;
|
||||
use App\Jobs\Tasks\GenerateShopeeAIArticleTask;
|
||||
use App\Models\Category;
|
||||
use App\Models\ShopeeSellerScrape;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ScraperTestController extends Controller
|
||||
{
|
||||
public function scrape(Request $request)
|
||||
{
|
||||
$category = Category::find(2);
|
||||
|
||||
$task = ShopeeSellerTopProductScraperJob::dispatch($request->input('seller'), 'MY', $category)
|
||||
->onQueue('default')
|
||||
->onConnection('default');
|
||||
}
|
||||
|
||||
public function gen(Request $request)
|
||||
{
|
||||
$shopee_seller_scrape = ShopeeSellerScrape::find(6);
|
||||
|
||||
$task = GenerateShopeeAIArticleTask::handle($shopee_seller_scrape);
|
||||
|
||||
dd($task);
|
||||
}
|
||||
}
|
||||
55
app/Jobs/ShopeeSellerTopProductScraperJob.php
Normal file
55
app/Jobs/ShopeeSellerTopProductScraperJob.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Tasks\GenerateShopeeAIArticleTask;
|
||||
use App\Jobs\Tasks\SaveShopeeSellerImagesTask;
|
||||
use App\Jobs\Tasks\ShopeeSellerTopProductScraperTask;
|
||||
use App\Models\Category;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ShopeeSellerTopProductScraperJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
protected $seller;
|
||||
|
||||
protected $country_iso;
|
||||
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(string $seller, string $country_iso, Category $category)
|
||||
{
|
||||
$this->seller = $seller;
|
||||
|
||||
$this->country_iso = $country_iso;
|
||||
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$shopee_task = ShopeeSellerTopProductScraperTask::handle($this->seller, $this->country_iso, $this->category);
|
||||
|
||||
//dd($shopee_task->product_task);
|
||||
|
||||
if (! is_null($shopee_task)) {
|
||||
SaveShopeeSellerImagesTask::handle($shopee_task);
|
||||
|
||||
GenerateShopeeAIArticleTask::handle($shopee_task->shopee_seller_scrape);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
181
app/Jobs/Tasks/GenerateShopeeAIArticleTask.php
Normal file
181
app/Jobs/Tasks/GenerateShopeeAIArticleTask.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use andreskrey\Readability\Configuration as ReadabilityConfiguration;
|
||||
use andreskrey\Readability\ParseException as ReadabilityParseException;
|
||||
use andreskrey\Readability\Readability;
|
||||
use App\Helpers\FirstParty\OpenAI\OpenAI;
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Models\AiWriteup;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostCategory;
|
||||
use App\Models\ShopeeSellerScrape;
|
||||
use App\Models\ShopeeSellerScrapedImage;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LaravelFreelancerNL\LaravelIndexNow\Facades\IndexNow;
|
||||
use LaravelGoogleIndexing;
|
||||
use Masterminds\HTML5;
|
||||
|
||||
class GenerateShopeeAIArticleTask
|
||||
{
|
||||
public static function handle(ShopeeSellerScrape $shopee_seller_scrape)
|
||||
{
|
||||
$serialised = OSSUploader::readFile('r2', 'shopee/seller', $shopee_seller_scrape->filename);
|
||||
|
||||
$post = null;
|
||||
|
||||
$shopee_seller_scrape->load('category');
|
||||
|
||||
if (! is_empty($serialised)) {
|
||||
$shopee_task = unserialize($serialised);
|
||||
$shopee_task->shopee_seller_scrape = $shopee_seller_scrape;
|
||||
}
|
||||
|
||||
// dd($shopee_task);
|
||||
|
||||
// dd($shopee_task->product_task->response);
|
||||
|
||||
$raw_html = $shopee_task->product_task->response->raw_html;
|
||||
|
||||
$excerpt = self::stripHtml($raw_html);
|
||||
|
||||
$photos = ShopeeSellerScrapedImage::where('shopee_seller_scrape_id', $shopee_seller_scrape->id)->where('featured', false)->orderByRaw('RAND()')->take(3)->get()->pluck('image')->toArray();
|
||||
|
||||
$ai_writeup = AiWriteup::where('source', 'shopee')->where('source_url', $shopee_task->product_task->response->url)->first();
|
||||
|
||||
if (is_null($ai_writeup)) {
|
||||
$ai_output = OpenAI::writeProductArticle($excerpt, $photos);
|
||||
|
||||
if (is_null($ai_output)) {
|
||||
$e = new Exception('Failed to write: Missing ai_output');
|
||||
|
||||
Log::error(serialize($ai_writeup?->toArray()));
|
||||
inspector()->reportException($e);
|
||||
throw ($e);
|
||||
} else {
|
||||
// save
|
||||
$ai_writeup = new AiWriteup;
|
||||
$ai_writeup->source = 'shopee';
|
||||
$ai_writeup->source_url = $shopee_task->product_task->response->url;
|
||||
$ai_writeup->category_id = $shopee_seller_scrape->category->id;
|
||||
$ai_writeup->title = $ai_output->title;
|
||||
$ai_writeup->excerpt = $ai_output->excerpt;
|
||||
$ai_writeup->featured_image = '';
|
||||
$ai_writeup->body = $ai_output->body;
|
||||
$ai_writeup->cost = self::getTotalServiceCost($shopee_task);
|
||||
$ai_writeup->editor_format = 'markdown';
|
||||
|
||||
if ($ai_writeup->save()) {
|
||||
$featured_photo = ShopeeSellerScrapedImage::where('shopee_seller_scrape_id', $shopee_seller_scrape->id)->where('featured', true)->first();
|
||||
|
||||
// new post
|
||||
$post_data = [
|
||||
'publish_date' => now(),
|
||||
'title' => $ai_writeup->title,
|
||||
'slug' => str_slug($ai_writeup->title),
|
||||
'excerpt' => $ai_writeup->excerpt,
|
||||
'cliffhanger' => $ai_writeup->cliffhanger,
|
||||
'author_id' => 1,
|
||||
'featured' => false,
|
||||
'featured_image' => $featured_photo->image,
|
||||
'editor' => 'markdown',
|
||||
'body' => $ai_writeup->body,
|
||||
'post_format' => 'standard',
|
||||
'status' => 'publish',
|
||||
];
|
||||
|
||||
$post = Post::create($post_data);
|
||||
|
||||
if (! is_null($post)) {
|
||||
PostCategory::create([
|
||||
'post_id' => $post->id,
|
||||
'category_id' => $shopee_seller_scrape->category->id,
|
||||
]);
|
||||
|
||||
if (app()->environment() == 'production') {
|
||||
if ($post->status == 'publish') {
|
||||
|
||||
$post_url = route('home.country.post', ['country' => $post->post_category?->category?->country_locale_slug, 'post_slug' => $post->slug]);
|
||||
|
||||
LaravelGoogleIndexing::create()->update($post_url);
|
||||
IndexNow::submit($post_url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$e = new Exception('Failed to write: ai_writeup found');
|
||||
Log::error(serialize($ai_writeup?->toArray()));
|
||||
inspector()->reportException($e);
|
||||
throw ($e);
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
private static function getTotalServiceCost($shopee_task)
|
||||
{
|
||||
|
||||
$cost = 0.00;
|
||||
|
||||
$cost += 0.06; // chatgpt-3.5-turbo $0.03 for 1k, writing for 2k tokens
|
||||
|
||||
// Shopee Seller Scraping
|
||||
if (isset($shopee_task?->seller_shop_task?->response?->total_cost)) {
|
||||
$cost += $shopee_task?->seller_shop_task?->response?->total_cost;
|
||||
}
|
||||
|
||||
// Shopee Product Scraping
|
||||
if (isset($shopee_task?->product_task?->response?->total_cost)) {
|
||||
$cost += $shopee_task?->product_task?->response?->total_cost;
|
||||
}
|
||||
|
||||
return $cost;
|
||||
|
||||
}
|
||||
|
||||
private static function stripHtml(string $raw_html)
|
||||
{
|
||||
$r_configuration = new ReadabilityConfiguration();
|
||||
$r_configuration->setWordThreshold(20);
|
||||
|
||||
$readability = new Readability($r_configuration);
|
||||
|
||||
// try {
|
||||
// $readability->parse($raw_html);
|
||||
|
||||
// $html_content = $readability->getContent();
|
||||
|
||||
// // Remove tabs
|
||||
// $html_content = str_replace("\t", '', $html_content);
|
||||
|
||||
// // Replace newlines with spaces
|
||||
// $html_content = str_replace(["\n", "\r\n"], ' ', $html_content);
|
||||
|
||||
// // Replace multiple spaces with a single space
|
||||
// $html_content = preg_replace('/\s+/', ' ', $html_content);
|
||||
|
||||
// // Output the cleaned text
|
||||
// $html_content = trim($html_content); // Using trim to remove any leading or trailing spaces
|
||||
|
||||
// $html_content = strip_tags($html_content);
|
||||
|
||||
// } catch (ReadabilityParseException|Exception $e) {
|
||||
|
||||
$html5 = new HTML5(['preserveWhiteSpace' => true]);
|
||||
|
||||
// Parse the HTML into a DOM tree.
|
||||
$dom = $html5->loadHTML($raw_html);
|
||||
|
||||
// Serialize the DOM tree back to a string, formatted.
|
||||
$html_content = strip_tags($html5->saveHTML($dom));
|
||||
|
||||
// }
|
||||
|
||||
return $html_content;
|
||||
}
|
||||
}
|
||||
355
app/Jobs/Tasks/SaveShopeeSellerImagesTask.php
Normal file
355
app/Jobs/Tasks/SaveShopeeSellerImagesTask.php
Normal file
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Models\ShopeeSellerScrapedImage;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class SaveShopeeSellerImagesTask
|
||||
{
|
||||
public static function handle($shopee_task)
|
||||
{
|
||||
$main_intervention_image = null;
|
||||
$intervention_images = [];
|
||||
$costs = [];
|
||||
|
||||
$main_image_url = null;
|
||||
|
||||
$proxy_server = get_smartproxy_server();
|
||||
$user_agent = config('platform.proxy.user_agent');
|
||||
|
||||
///////// PART 1
|
||||
|
||||
// If there is a main intervention image, then set in, else get the url only.
|
||||
if (isset($shopee_task?->product_task?->intervention?->main_intervention_image)) {
|
||||
$main_intervention_image = $shopee_task->product_task->intervention->main_intervention_image;
|
||||
} else {
|
||||
$main_image_url = self::getProductImageUrl($shopee_task->product_task->response->jsonld);
|
||||
}
|
||||
|
||||
// If there is other image interventions set, then set in, else get the image urls only.
|
||||
if (isset($shopee_task?->product_task?->intervention?->intervention_images)) {
|
||||
$intervention_images = $shopee_task->product_task->intervention->intervention_images;
|
||||
} else {
|
||||
$images = self::getImages($shopee_task->product_task->response->raw_html);
|
||||
$images = self::filterImages($images, $proxy_server, $user_agent, $costs, $intervention_images);
|
||||
}
|
||||
|
||||
///////// PART 2
|
||||
|
||||
// Check existence and upload if image intervention is set
|
||||
if (! is_null($main_intervention_image)) {
|
||||
$scraped_image = ShopeeSellerScrapedImage::where('original_name', $main_intervention_image->original_name)->where('shopee_seller_scrape_id', $shopee_task->shopee_seller_scrape->id)->first();
|
||||
|
||||
if (is_null($scraped_image)) {
|
||||
$scraped_image = self::uploadAndSaveScrapedImage($shopee_task->shopee_seller_scrape, $main_intervention_image, true);
|
||||
}
|
||||
}
|
||||
// if there is no main image intervention but the main image url is provided
|
||||
elseif (! is_empty($main_image_url)) {
|
||||
$scraped_image = ShopeeSellerScrapedImage::where('original_name', pathinfo($main_image_url, PATHINFO_BASENAME))->where('shopee_seller_scrape_id', $shopee_task->shopee_seller_scrape->id)->first();
|
||||
|
||||
if (is_null($scraped_image)) {
|
||||
$main_image = self::getProductImage($shopee_task->product_task->response->jsonld, $proxy_server, $user_agent, $costs, $main_intervention_image);
|
||||
|
||||
$scraped_image = self::uploadAndSaveScrapedImage($shopee_task->shopee_seller_scrape, $main_intervention_image, true);
|
||||
}
|
||||
}
|
||||
|
||||
/////// PART 3
|
||||
|
||||
if (! is_null($intervention_images) && is_array($intervention_images) && count($intervention_images) > 0) {
|
||||
foreach ($intervention_images as $intervention_image) {
|
||||
$scraped_image = ShopeeSellerScrapedImage::where('original_name', $intervention_image->original_name)->where('shopee_seller_scrape_id', $shopee_task->shopee_seller_scrape->id)->first();
|
||||
|
||||
if (is_null($scraped_image)) {
|
||||
$scraped_image = self::uploadAndSaveScrapedImage($shopee_task->shopee_seller_scrape, $intervention_image, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return ShopeeSellerScrapedImage::where('shopee_seller_scrape_id', $shopee_task->shopee_seller_scrape->id)->get();
|
||||
|
||||
}
|
||||
|
||||
private static function uploadAndSaveScrapedImage($shopee_seller_scrape, $intervention_image, $featured = false)
|
||||
{
|
||||
// Generate a unique filename for the uploaded file and LQIP version
|
||||
$uuid = Str::uuid()->toString();
|
||||
$fileName = time().'_'.$uuid.'.jpg';
|
||||
$lqipFileName = time().'_'.$uuid.'_lqip.jpg';
|
||||
|
||||
// Convert the file to JPEG format using Intervention Image library
|
||||
$image = $intervention_image->image;
|
||||
|
||||
// Get the original image width and height
|
||||
$originalWidth = $image->width();
|
||||
$originalHeight = $image->height();
|
||||
|
||||
// Compress the image to reduce file size to 50%
|
||||
$image->encode('jpg', 50);
|
||||
|
||||
// Save the processed image to the 'r2' storage driver under the 'uploads' directory
|
||||
$filePath = 'uploads/'.$fileName;
|
||||
$lqipFilePath = 'uploads/'.$lqipFileName;
|
||||
Storage::disk('r2')->put($filePath, $image->stream()->detach());
|
||||
|
||||
// Save the original image to a temporary file and open it again
|
||||
$tempImagePath = tempnam(sys_get_temp_dir(), 'temp_image');
|
||||
file_put_contents($tempImagePath, $intervention_image->image->encode());
|
||||
$clonedImage = Image::make($tempImagePath);
|
||||
|
||||
// Create the LQIP version of the image using a small size while maintaining the aspect ratio
|
||||
$lqipImage = $clonedImage->fit(10, 10, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$lqipImage->encode('jpg', 5);
|
||||
Storage::disk('r2')->put($lqipFilePath, $lqipImage->stream()->detach());
|
||||
|
||||
// Cleanup the temporary image file
|
||||
unlink($tempImagePath);
|
||||
|
||||
// Get the final URL of the uploaded image (non-LQIP version)
|
||||
$url = Storage::disk('r2')->url($filePath);
|
||||
|
||||
$scraped_image = new ShopeeSellerScrapedImage;
|
||||
$scraped_image->shopee_seller_scrape_id = $shopee_seller_scrape->id;
|
||||
$scraped_image->original_name = $intervention_image->original_name;
|
||||
$scraped_image->image = $url;
|
||||
$scraped_image->featured = $featured;
|
||||
|
||||
if ($scraped_image->save()) {
|
||||
return $scraped_image;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function getImages(string $raw_html)
|
||||
{
|
||||
$crawler = new Crawler($raw_html);
|
||||
$images = [];
|
||||
|
||||
$crawler->filter('img')->each(function ($node) use (&$images) {
|
||||
$src = $node->attr('src');
|
||||
$alt = $node->attr('alt') ?? null; // Setting a default value if alt is not present
|
||||
$images[] = [
|
||||
'src' => $src,
|
||||
'alt' => $alt,
|
||||
];
|
||||
});
|
||||
|
||||
// if (count($images) > 4)
|
||||
// {
|
||||
// return $images;
|
||||
// }
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
private static function filterImages(array $images, string $proxy, string $user_agent, &$costs, &$intervention_images)
|
||||
{
|
||||
$filteredImages = [];
|
||||
$uniqueAttributes = []; // This array will track unique width, height, mime, and size combinations
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($images as $image) {
|
||||
$count++;
|
||||
|
||||
$src = $image['src'];
|
||||
|
||||
try {
|
||||
$response = Http::withOptions(['proxy' => $proxy])->withHeaders(['User-Agent' => $user_agent])->get($src);
|
||||
|
||||
// Check if the request was successful
|
||||
if (! $response->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageData = $response->body();
|
||||
|
||||
// Create an Intervention Image instance from the response data
|
||||
$interventionImage = Image::make($imageData);
|
||||
|
||||
$width = $interventionImage->width();
|
||||
$height = $interventionImage->height();
|
||||
$mime = $interventionImage->mime();
|
||||
|
||||
// Image size in KB
|
||||
$sizeKb = round(strlen($imageData) / 1024, 2);
|
||||
|
||||
// Check constraints
|
||||
if ($width < 800 || $height < 800 || $sizeKb < 100 || $mime !== 'image/jpeg') {
|
||||
continue;
|
||||
}
|
||||
$image['width'] = $width;
|
||||
$image['height'] = $height;
|
||||
$image['mime'] = $mime;
|
||||
$image['sizeKb'] = $sizeKb;
|
||||
|
||||
// Check for duplicates by searching through uniqueAttributes
|
||||
$isDuplicate = false;
|
||||
foreach ($uniqueAttributes as $attr) {
|
||||
if (
|
||||
$attr['width'] == $width &&
|
||||
$attr['height'] == $height &&
|
||||
$attr['mime'] == $mime &&
|
||||
abs($attr['sizeKb'] - $sizeKb) <= 30 // Check for size within a +/- 10kB tolerance
|
||||
) {
|
||||
$isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $isDuplicate) {
|
||||
$uniqueAttributes[] = [
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'mime' => $mime,
|
||||
'sizeKb' => $sizeKb,
|
||||
];
|
||||
$image['color_counts'] = self::isMostlyTextBasedOnUniqueColors($interventionImage);
|
||||
//$image['img'] = $interventionImage;
|
||||
$costs['count-'.$count] = calculate_smartproxy_cost($sizeKb);
|
||||
|
||||
$filteredImages[] = $image;
|
||||
|
||||
$intervention_images[] = (object) [
|
||||
'image' => $interventionImage,
|
||||
'original_name' => pathinfo($src, PATHINFO_BASENAME),
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Handle exceptions related to the HTTP request
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all the color counts
|
||||
$colorCounts = [];
|
||||
foreach ($filteredImages as $image) {
|
||||
$colorCounts[] = $image['color_counts'];
|
||||
}
|
||||
|
||||
// Compute the median of the color counts
|
||||
sort($colorCounts);
|
||||
$count = count($colorCounts);
|
||||
$middleIndex = floor($count / 2);
|
||||
$median = $count % 2 === 0 ? ($colorCounts[$middleIndex - 1] + $colorCounts[$middleIndex]) / 2 : $colorCounts[$middleIndex];
|
||||
|
||||
// Use the median to filter out the low outliers
|
||||
$threshold = 0.10 * $median; // Adjust this percentage as needed
|
||||
$filteredImages = array_filter($filteredImages, function ($image) use ($threshold) {
|
||||
return $image['color_counts'] > $threshold;
|
||||
});
|
||||
|
||||
usort($filteredImages, function ($a, $b) {
|
||||
return $b['sizeKb'] <=> $a['sizeKb']; // Using the spaceship operator to sort in descending order
|
||||
});
|
||||
|
||||
return $filteredImages;
|
||||
}
|
||||
|
||||
private static function getProductImageUrl(array $jsonLdData)
|
||||
{
|
||||
foreach ($jsonLdData as $data) {
|
||||
// Ensure the type is "Product" before proceeding
|
||||
if (isset($data->{'@type'}) && $data->{'@type'} === 'Product') {
|
||||
if (isset($data->url)) {
|
||||
return $data->url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function getProductImage(array $jsonLdData, string $proxy, string $user_agent, &$costs, &$main_intervention_image)
|
||||
{
|
||||
foreach ($jsonLdData as $data) {
|
||||
// Ensure the type is "Product" before proceeding
|
||||
if (isset($data->{'@type'}) && $data->{'@type'} === 'Product') {
|
||||
if (isset($data->url) && isset($data->image)) {
|
||||
try {
|
||||
$response = Http::withOptions(['proxy' => $proxy])->withHeaders(['User-Agent' => $user_agent])->get($data->image);
|
||||
|
||||
// Check if the request was successful
|
||||
if ($response->successful()) {
|
||||
$imageData = $response->body();
|
||||
|
||||
// Create an Intervention Image instance from the response data
|
||||
$interventionImage = Image::make($imageData);
|
||||
|
||||
// Resize/upscale the image to 1920x1080 maintaining the aspect ratio and cropping if needed
|
||||
$interventionImage->fit(1920, 1080, function ($constraint) {
|
||||
$constraint->upsize();
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
$sizeInKb = strlen($interventionImage->encode()) / 1024; // Convert bytes to kilobytes
|
||||
|
||||
// Calculate the cost
|
||||
$cost = calculate_smartproxy_cost($sizeInKb);
|
||||
|
||||
$costs['product_image'] = $cost;
|
||||
|
||||
$main_intervention_image = (object) [
|
||||
'image' => $interventionImage,
|
||||
'original_name' => pathinfo($data->image, PATHINFO_BASENAME),
|
||||
];
|
||||
|
||||
return [
|
||||
'url' => $data->url,
|
||||
//'img' => $interventionImage,
|
||||
'cost' => $cost,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Handle exceptions related to the HTTP request
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isMostlyTextBasedOnUniqueColors($interventionImage)
|
||||
{
|
||||
// Use Intervention to manipulate the image
|
||||
$img = clone $interventionImage;
|
||||
|
||||
// Resize to a smaller dimension for faster processing (maintaining aspect ratio)
|
||||
$img->resize(200, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
// Apply some blur
|
||||
$img->blur(10);
|
||||
|
||||
$im = imagecreatefromstring($img->encode());
|
||||
|
||||
$width = imagesx($im);
|
||||
$height = imagesy($im);
|
||||
|
||||
$uniqueColors = [];
|
||||
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$rgb = imagecolorat($im, $x, $y);
|
||||
$uniqueColors[$rgb] = true;
|
||||
}
|
||||
}
|
||||
|
||||
imagedestroy($im);
|
||||
|
||||
// Adjust the threshold based on your dataset.
|
||||
// Here, I'm assuming that images with less than 100 unique colors are mostly text
|
||||
// because we've reduced the image size and applied blurring.
|
||||
return count($uniqueColors);
|
||||
}
|
||||
}
|
||||
133
app/Jobs/Tasks/ShopeeSellerTopProductScraperTask.php
Normal file
133
app/Jobs/Tasks/ShopeeSellerTopProductScraperTask.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Models\Category;
|
||||
use App\Models\ShopeeSellerScrape;
|
||||
use Exception;
|
||||
|
||||
class ShopeeSellerTopProductScraperTask
|
||||
{
|
||||
public static function handle(string $seller, string $country_iso, Category $category)
|
||||
{
|
||||
|
||||
$country_iso = strtolower($country_iso);
|
||||
|
||||
if (is_empty($seller)) {
|
||||
throw new Exception('Missing \'seller\' attribute.');
|
||||
}
|
||||
|
||||
$shopee_seller_scrape = ShopeeSellerScrape::where('seller', $seller)
|
||||
->where('country_iso', $country_iso)->first();
|
||||
|
||||
if (! is_null($shopee_seller_scrape)) {
|
||||
$serialised = OSSUploader::readFile('r2', 'shopee/seller', $shopee_seller_scrape->filename);
|
||||
|
||||
if (! is_empty($serialised)) {
|
||||
$obj = unserialize($serialised);
|
||||
$obj->shopee_seller_scrape = $shopee_seller_scrape;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
|
||||
$epoch = epoch_now_timestamp();
|
||||
|
||||
$seller_shop_url = "https://shopee.com.my/{$seller}?page=0&sortBy=sales";
|
||||
|
||||
$seller_shop_task = UrlCrawlerTask::handle($seller_shop_url, 'shopee/seller', $epoch, true, false);
|
||||
|
||||
//dd($seller_shop_task);
|
||||
|
||||
if (isset($seller_shop_task->response->jsonld)) {
|
||||
$top_rank_products = self::getSortedData($seller_shop_task->response->jsonld, 100);
|
||||
|
||||
if (count($top_rank_products) > 0) {
|
||||
|
||||
$product_found = null;
|
||||
|
||||
foreach ($top_rank_products as $product) {
|
||||
$product_task = UrlCrawlerTask::handle($product->url, 'shopee/seller', $epoch, true, true);
|
||||
|
||||
if ($product_task->response->status_code >= 0) {
|
||||
$product_found = $product_task->response;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scraped = (object) [
|
||||
'seller_shop_task' => (object) [
|
||||
'response' => $seller_shop_task->response,
|
||||
],
|
||||
'product_task' => (object) [
|
||||
'response' => $product_task->response,
|
||||
],
|
||||
];
|
||||
|
||||
$serialised = serialize($scraped);
|
||||
|
||||
$filename = $seller.'-'.$epoch.'-'.$country_iso.'.txt';
|
||||
|
||||
OSSUploader::uploadFile('r2', 'shopee/seller', $filename, $serialised);
|
||||
|
||||
$shopee_seller_scrape = new ShopeeSellerScrape;
|
||||
$shopee_seller_scrape->seller = $seller;
|
||||
$shopee_seller_scrape->country_iso = $country_iso;
|
||||
$shopee_seller_scrape->epoch = $epoch;
|
||||
$shopee_seller_scrape->filename = $filename;
|
||||
$shopee_seller_scrape->category_id = $category->id;
|
||||
|
||||
if ($shopee_seller_scrape->save()) {
|
||||
return (object) compact('seller_shop_task', 'product_task', 'shopee_seller_scrape');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function getSortedData($data, $minValue)
|
||||
{
|
||||
// Filter the items of type "Product" with an offer price greater than 200
|
||||
$filtered = array_filter($data, function ($item) use ($minValue) {
|
||||
$isProduct = $item->{'@type'} === 'Product';
|
||||
$lowPrice = floatval($item->offers?->lowPrice ?? 0);
|
||||
$price = floatval($item->offers?->price ?? 0);
|
||||
|
||||
return $isProduct && ($lowPrice > $minValue) || ($price > $minValue);
|
||||
});
|
||||
|
||||
// Sort the items based on `ratingCount` and `ratingValue` in descending order
|
||||
usort($filtered, function ($a, $b) {
|
||||
$ratingCountA = intval($a->aggregateRating?->ratingCount ?? 0);
|
||||
$ratingCountB = intval($b->aggregateRating?->ratingCount ?? 0);
|
||||
|
||||
$ratingValueA = floatval($a->aggregateRating?->ratingValue ?? 0);
|
||||
$ratingValueB = floatval($b->aggregateRating?->ratingValue ?? 0);
|
||||
|
||||
if ($ratingCountA !== $ratingCountB) {
|
||||
return $ratingCountB - $ratingCountA;
|
||||
}
|
||||
|
||||
return $ratingValueB <=> $ratingValueA;
|
||||
});
|
||||
|
||||
// Map the filtered and sorted items to a new array of objects
|
||||
return array_map(function ($item) {
|
||||
return (object) [
|
||||
'name' => $item->name ?? null,
|
||||
'description' => $item->description ?? null,
|
||||
'url' => $item->url ?? null,
|
||||
'image' => $item->image ?? null,
|
||||
'lowPrice' => floatval($item->offers?->lowPrice ?? 0),
|
||||
'highPrice' => floatval($item->offers?->highPrice ?? 0),
|
||||
'price' => floatval($item->offers?->price ?? 0),
|
||||
'priceCurrency' => $item->offers?->priceCurrency ?? null,
|
||||
'ratingCount' => intval($item->aggregateRating?->ratingCount ?? 0),
|
||||
'ratingValue' => floatval($item->aggregateRating?->ratingValue ?? 0),
|
||||
];
|
||||
}, $filtered);
|
||||
}
|
||||
}
|
||||
448
app/Jobs/Tasks/UrlCrawlerTask.php
Normal file
448
app/Jobs/Tasks/UrlCrawlerTask.php
Normal file
@@ -0,0 +1,448 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Minifier\TinyMinify;
|
||||
use Spatie\Browsershot\Browsershot;
|
||||
use Spatie\Browsershot\Exceptions\UnsuccessfulResponse;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use thiagoalessio\TesseractOCR\TesseractOCR;
|
||||
|
||||
class UrlCrawlerTask
|
||||
{
|
||||
public static function handle(string $url, $directory, $postfix = null, $strip_html = false, $parse_images = false)
|
||||
{
|
||||
$slug = str_slug($url);
|
||||
|
||||
$cached_url = self::getGoogleCachedUrl($url, false);
|
||||
|
||||
$postfix = strval($postfix);
|
||||
|
||||
$driver = 'r2';
|
||||
$filename = $slug.'-'.$postfix.'.html';
|
||||
$user_agent = config('platform.proxy.user_agent');
|
||||
$disk_url = $directory.$filename;
|
||||
|
||||
$raw_html = null;
|
||||
$status_code = 0;
|
||||
|
||||
$costs = [];
|
||||
|
||||
$main_intervention_image = null;
|
||||
$intervention_images = [];
|
||||
|
||||
$proxy_server = get_smartproxy_server();
|
||||
|
||||
try {
|
||||
$raw_html = OSSUploader::readFile($driver, $directory, $filename);
|
||||
|
||||
if (is_null($raw_html)) {
|
||||
$status_code = -1;
|
||||
throw new Exception('Not stored.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$raw_html = null;
|
||||
}
|
||||
|
||||
if (is_null($raw_html)) {
|
||||
|
||||
try {
|
||||
$browsershot = new Browsershot();
|
||||
|
||||
$browsershot->setUrl($cached_url)
|
||||
->setOption('args', ['headless: "new"'])
|
||||
->noSandbox()
|
||||
->setOption('args', ['--disable-web-security'])
|
||||
->userAgent($user_agent)
|
||||
->ignoreHttpsErrors()
|
||||
->preventUnsuccessfulResponse()
|
||||
->timeout(10)
|
||||
//->setProxyServer($proxy_server)
|
||||
->userAgent($user_agent);
|
||||
|
||||
if (app()->environment() == 'local') {
|
||||
$browsershot->setNodeBinary(config('platform.general.node_binary'))->setNpmBinary(config('platform.general.npm_binary'));
|
||||
}
|
||||
|
||||
//dump($browsershot);
|
||||
|
||||
$raw_html = $browsershot->bodyHtml();
|
||||
|
||||
// $sizeInKb = strlen($raw_html) / 1024; // Convert bytes to kilobytes
|
||||
// $browsershot_cost = round(calculate_smartproxy_cost($sizeInKb)) ;
|
||||
|
||||
// $costs['html'] = $browsershot_cost;
|
||||
|
||||
} catch (UnsuccessfulResponse|Exception $e) {
|
||||
$raw_html = null;
|
||||
$status_code = -3;
|
||||
}
|
||||
|
||||
if (! is_empty($raw_html)) {
|
||||
OSSUploader::uploadFile($driver, $directory, $filename, $raw_html);
|
||||
$status_code = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (! is_null($raw_html)) {
|
||||
|
||||
$raw_html = self::minifyAndCleanHtml($raw_html);
|
||||
|
||||
$jsonld = self::getJsonLd($raw_html);
|
||||
|
||||
if ($parse_images) {
|
||||
$images = self::getImages($raw_html);
|
||||
$images = self::filterImages($images, $proxy_server, $user_agent, $costs, $intervention_images);
|
||||
} else {
|
||||
$images = [];
|
||||
}
|
||||
|
||||
$main_image = self::getProductImage($jsonld, $proxy_server, $user_agent, $costs, $main_intervention_image);
|
||||
|
||||
return (object) [
|
||||
'intervention' => (object) compact('main_intervention_image', 'intervention_images'),
|
||||
'response' => (object) [
|
||||
'url' => $url,
|
||||
'postfix' => $postfix,
|
||||
'filename' => $disk_url,
|
||||
'raw_html' => $raw_html,
|
||||
'jsonld' => $jsonld,
|
||||
'main_image' => $main_image,
|
||||
'images' => $images,
|
||||
'status_code' => $status_code,
|
||||
'costs' => $costs,
|
||||
'total_cost' => array_sum(array_values($costs)),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'response' => (object) [
|
||||
'url' => $url,
|
||||
'postfix' => $postfix,
|
||||
'filename' => null,
|
||||
'raw_html' => null,
|
||||
'jsonld' => [],
|
||||
'main_image' => null,
|
||||
'images' => [],
|
||||
'status_code' => $status_code,
|
||||
'costs' => $costs,
|
||||
'total_cost' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function getJsonLd(string $raw_html)
|
||||
{
|
||||
$crawler = new Crawler($raw_html);
|
||||
|
||||
try {
|
||||
$jsonld = $crawler->filter('script[type="application/ld+json"]')->each(function (Crawler $node) {
|
||||
return $node->text();
|
||||
});
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$contents = [];
|
||||
|
||||
foreach ($jsonld as $content) {
|
||||
try {
|
||||
$contents[] = json_decode($content);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
private static function getImages(string $raw_html)
|
||||
{
|
||||
$crawler = new Crawler($raw_html);
|
||||
$images = [];
|
||||
|
||||
$crawler->filter('img')->each(function ($node) use (&$images) {
|
||||
$src = $node->attr('src');
|
||||
$alt = $node->attr('alt') ?? null; // Setting a default value if alt is not present
|
||||
$images[] = [
|
||||
'src' => $src,
|
||||
'alt' => $alt,
|
||||
];
|
||||
});
|
||||
|
||||
// if (count($images) > 4)
|
||||
// {
|
||||
// return $images;
|
||||
// }
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
private static function filterImages(array $images, string $proxy, string $user_agent, &$costs, &$intervention_images)
|
||||
{
|
||||
$filteredImages = [];
|
||||
$uniqueAttributes = []; // This array will track unique width, height, mime, and size combinations
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($images as $image) {
|
||||
$count++;
|
||||
|
||||
$src = $image['src'];
|
||||
|
||||
try {
|
||||
$response = Http::withOptions(['proxy' => $proxy])->withHeaders(['User-Agent' => $user_agent])->get($src);
|
||||
|
||||
// Check if the request was successful
|
||||
if (! $response->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$imageData = $response->body();
|
||||
|
||||
// Create an Intervention Image instance from the response data
|
||||
$interventionImage = Image::make($imageData);
|
||||
|
||||
$width = $interventionImage->width();
|
||||
$height = $interventionImage->height();
|
||||
$mime = $interventionImage->mime();
|
||||
|
||||
// Image size in KB
|
||||
$sizeKb = round(strlen($imageData) / 1024, 2);
|
||||
|
||||
// Check constraints
|
||||
if ($width < 800 || $height < 800 || $sizeKb < 100 || $mime !== 'image/jpeg') {
|
||||
continue;
|
||||
}
|
||||
$image['width'] = $width;
|
||||
$image['height'] = $height;
|
||||
$image['mime'] = $mime;
|
||||
$image['sizeKb'] = $sizeKb;
|
||||
|
||||
// Check for duplicates by searching through uniqueAttributes
|
||||
$isDuplicate = false;
|
||||
foreach ($uniqueAttributes as $attr) {
|
||||
if (
|
||||
$attr['width'] == $width &&
|
||||
$attr['height'] == $height &&
|
||||
$attr['mime'] == $mime &&
|
||||
abs($attr['sizeKb'] - $sizeKb) <= 30 // Check for size within a +/- 10kB tolerance
|
||||
) {
|
||||
$isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $isDuplicate) {
|
||||
$uniqueAttributes[] = [
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'mime' => $mime,
|
||||
'sizeKb' => $sizeKb,
|
||||
];
|
||||
$image['color_counts'] = self::isMostlyTextBasedOnUniqueColors($interventionImage);
|
||||
//$image['img'] = $interventionImage;
|
||||
$costs['count-'.$count] = calculate_smartproxy_cost($sizeKb);
|
||||
|
||||
$filteredImages[] = $image;
|
||||
|
||||
$intervention_images[] = (object) [
|
||||
'image' => $interventionImage,
|
||||
'original_name' => pathinfo($src, PATHINFO_BASENAME),
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Handle exceptions related to the HTTP request
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all the color counts
|
||||
$colorCounts = [];
|
||||
foreach ($filteredImages as $image) {
|
||||
$colorCounts[] = $image['color_counts'];
|
||||
}
|
||||
|
||||
// Compute the median of the color counts
|
||||
sort($colorCounts);
|
||||
$count = count($colorCounts);
|
||||
$middleIndex = floor($count / 2);
|
||||
$median = $count % 2 === 0 ? ($colorCounts[$middleIndex - 1] + $colorCounts[$middleIndex]) / 2 : $colorCounts[$middleIndex];
|
||||
|
||||
// Use the median to filter out the low outliers
|
||||
$threshold = 0.10 * $median; // Adjust this percentage as needed
|
||||
$filteredImages = array_filter($filteredImages, function ($image) use ($threshold) {
|
||||
return $image['color_counts'] > $threshold;
|
||||
});
|
||||
|
||||
usort($filteredImages, function ($a, $b) {
|
||||
return $b['sizeKb'] <=> $a['sizeKb']; // Using the spaceship operator to sort in descending order
|
||||
});
|
||||
|
||||
return $filteredImages;
|
||||
}
|
||||
|
||||
// private static function isImageMostlyText($imageData, $mime) {
|
||||
// try {
|
||||
// $text = (new TesseractOCR)->imageData($imageData, $mime)->run();
|
||||
// $textLength = strlen($text);
|
||||
|
||||
// // This is a basic check. Adjust the threshold as needed.
|
||||
// return $textLength > 50;
|
||||
// } catch (\Exception $e) {
|
||||
// // Handle any exceptions related to Tesseract OCR
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
private static function getProductImage(array $jsonLdData, string $proxy, string $user_agent, &$costs, &$main_intervention_image)
|
||||
{
|
||||
foreach ($jsonLdData as $data) {
|
||||
// Ensure the type is "Product" before proceeding
|
||||
if (isset($data->{'@type'}) && $data->{'@type'} === 'Product') {
|
||||
if (isset($data->url) && isset($data->image)) {
|
||||
try {
|
||||
$response = Http::withOptions(['proxy' => $proxy])->withHeaders(['User-Agent' => $user_agent])->get($data->image);
|
||||
|
||||
// Check if the request was successful
|
||||
if ($response->successful()) {
|
||||
$imageData = $response->body();
|
||||
|
||||
// Create an Intervention Image instance from the response data
|
||||
$interventionImage = Image::make($imageData);
|
||||
|
||||
// Resize/upscale the image to 1920x1080 maintaining the aspect ratio and cropping if needed
|
||||
$interventionImage->fit(1920, 1080, function ($constraint) {
|
||||
$constraint->upsize();
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
$sizeInKb = strlen($interventionImage->encode()) / 1024; // Convert bytes to kilobytes
|
||||
|
||||
// Calculate the cost
|
||||
$cost = calculate_smartproxy_cost($sizeInKb);
|
||||
|
||||
$costs['product_image'] = $cost;
|
||||
|
||||
$main_intervention_image = (object) [
|
||||
'image' => $interventionImage,
|
||||
'original_name' => pathinfo($data->image, PATHINFO_BASENAME),
|
||||
];
|
||||
|
||||
return [
|
||||
'url' => $data->url,
|
||||
//'img' => $interventionImage,
|
||||
'cost' => $cost,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Handle exceptions related to the HTTP request
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function isMostlyTextBasedOnUniqueColors($interventionImage)
|
||||
{
|
||||
// Use Intervention to manipulate the image
|
||||
$img = clone $interventionImage;
|
||||
|
||||
// Resize to a smaller dimension for faster processing (maintaining aspect ratio)
|
||||
$img->resize(200, null, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
// Apply some blur
|
||||
$img->blur(10);
|
||||
|
||||
$im = imagecreatefromstring($img->encode());
|
||||
|
||||
$width = imagesx($im);
|
||||
$height = imagesy($im);
|
||||
|
||||
$uniqueColors = [];
|
||||
|
||||
for ($x = 0; $x < $width; $x++) {
|
||||
for ($y = 0; $y < $height; $y++) {
|
||||
$rgb = imagecolorat($im, $x, $y);
|
||||
$uniqueColors[$rgb] = true;
|
||||
}
|
||||
}
|
||||
|
||||
imagedestroy($im);
|
||||
|
||||
// Adjust the threshold based on your dataset.
|
||||
// Here, I'm assuming that images with less than 100 unique colors are mostly text
|
||||
// because we've reduced the image size and applied blurring.
|
||||
return count($uniqueColors);
|
||||
}
|
||||
|
||||
private static function minifyAndCleanHtml(string $raw_html)
|
||||
{
|
||||
$raw_html = TinyMinify::html($raw_html);
|
||||
|
||||
$crawler = new Crawler($raw_html);
|
||||
|
||||
// Directly loop through the DOM and remove 'class' and 'id' attributes
|
||||
foreach ($crawler as $domElement) {
|
||||
/** @var \DOMNodeList $nodes */
|
||||
$nodes = $domElement->getElementsByTagName('*');
|
||||
foreach ($nodes as $node) {
|
||||
/** @var \DOMElement $node */
|
||||
$node->removeAttribute('class');
|
||||
$node->removeAttribute('id');
|
||||
$node->removeAttribute('style');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove <style> tags and their content
|
||||
$styleTags = $domElement->getElementsByTagName('style');
|
||||
for ($i = $styleTags->length; --$i >= 0;) {
|
||||
$styleNode = $styleTags->item($i);
|
||||
$styleNode->parentNode->removeChild($styleNode);
|
||||
}
|
||||
|
||||
// Output the manipulated HTML
|
||||
return $crawler->html();
|
||||
}
|
||||
|
||||
private static function getGoogleCachedUrl(string $url, $stripHtml = false)
|
||||
{
|
||||
$url = self::stripUrlQueryParameters($url);
|
||||
$cached_url = "https://webcache.googleusercontent.com/search?q=cache:{$url}";
|
||||
|
||||
if ($stripHtml) {
|
||||
$cached_url .= '&strip=1';
|
||||
}
|
||||
|
||||
return $cached_url;
|
||||
|
||||
}
|
||||
|
||||
private static function stripUrlQueryParameters(string $url)
|
||||
{
|
||||
// Parse the URL into its components
|
||||
$parts = parse_url($url);
|
||||
|
||||
// Rebuild the URL without the query component
|
||||
$newUrl = $parts['scheme'].'://'.$parts['host'];
|
||||
|
||||
if (isset($parts['path'])) {
|
||||
$newUrl .= $parts['path'];
|
||||
}
|
||||
|
||||
if (isset($parts['fragment'])) {
|
||||
$newUrl .= '#'.$parts['fragment'];
|
||||
}
|
||||
|
||||
return $newUrl;
|
||||
}
|
||||
}
|
||||
55
app/Models/AiWriteup.php
Normal file
55
app/Models/AiWriteup.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class AiWriteup
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $source
|
||||
* @property string $source_url
|
||||
* @property int $category_id
|
||||
* @property string $title
|
||||
* @property string $editor_format
|
||||
* @property string|null $excerpt
|
||||
* @property string|null $featured_image
|
||||
* @property array|null $body
|
||||
* @property float $cost
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Category $category
|
||||
*/
|
||||
class AiWriteup extends Model
|
||||
{
|
||||
protected $table = 'ai_writeups';
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'int',
|
||||
'body' => 'json',
|
||||
'cost' => 'float',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'source',
|
||||
'source_url',
|
||||
'category_id',
|
||||
'title',
|
||||
'editor_format',
|
||||
'excerpt',
|
||||
'featured_image',
|
||||
'body',
|
||||
'cost',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
*/
|
||||
class Category extends Model
|
||||
{
|
||||
use SoftDeletes, Cachable;
|
||||
use Cachable, SoftDeletes;
|
||||
|
||||
protected $table = 'categories';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*/
|
||||
class CountryLocale extends Model
|
||||
{
|
||||
use SoftDeletes, Cachable;
|
||||
use Cachable, SoftDeletes;
|
||||
|
||||
protected $table = 'country_locales';
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
|
||||
use AlAminFirdows\LaravelEditorJs\Facades\LaravelEditorJs;
|
||||
use Carbon\Carbon;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Feed\Feedable;
|
||||
use Spatie\Feed\FeedItem;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* Class Post
|
||||
@@ -85,7 +87,114 @@ public function post_category()
|
||||
public function getHtmlBodyAttribute()
|
||||
{
|
||||
if (! is_empty($this->body)) {
|
||||
return LaravelEditorJs::render(json_encode($this->body));
|
||||
if ($this->editor == 'editorjs') {
|
||||
return LaravelEditorJs::render(json_encode($this->body));
|
||||
} elseif ($this->editor == 'markdown') {
|
||||
//dd($this->body);
|
||||
|
||||
$html = Markdown::convert($this->body)->getContent();
|
||||
|
||||
//dd($html);
|
||||
|
||||
$html = str_replace('\\n', "\n", $html);
|
||||
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
$firstHeaderProcessed = false;
|
||||
|
||||
$crawler->filter('h1, h2, h3, h4, h5, h6')->each(function (Crawler $node) use (&$firstHeaderProcessed) {
|
||||
$element = $node->getNode(0);
|
||||
|
||||
// Create a new h3 element
|
||||
$newElement = $element->ownerDocument->createElement('h3');
|
||||
|
||||
// Copy attributes from the old header element to the new h3 element
|
||||
foreach ($element->attributes as $attribute) {
|
||||
$newElement->setAttribute($attribute->name, $attribute->value);
|
||||
}
|
||||
|
||||
// Add the 'fw-bold' class to the new h3 element
|
||||
$existingClasses = $element->getAttribute('class');
|
||||
$newClass = 'fw-bold';
|
||||
$updatedClasses = ($existingClasses ? $existingClasses.' ' : '').$newClass;
|
||||
$newElement->setAttribute('class', $updatedClasses);
|
||||
|
||||
// Move child nodes from the old header element to the new h3 element
|
||||
while ($element->firstChild) {
|
||||
$newElement->appendChild($element->firstChild);
|
||||
}
|
||||
|
||||
// Replace the old header element with the new h3 element in the DOM
|
||||
$element->parentNode->replaceChild($newElement, $element);
|
||||
|
||||
// Only for the first header
|
||||
if (! $firstHeaderProcessed) {
|
||||
$firstHeaderProcessed = true;
|
||||
|
||||
$nextSibling = $newElement->nextSibling;
|
||||
while ($nextSibling) {
|
||||
if ($nextSibling->nodeType === XML_ELEMENT_NODE) {
|
||||
if ($nextSibling->nodeName === 'p' && ($nextSibling->getElementsByTagName('img')->length > 0 || $nextSibling->getElementsByTagName('figure')->length > 0)) {
|
||||
// Remove <p> if it contains an <img> or <figure> directly
|
||||
$nextSibling->parentNode->removeChild($nextSibling);
|
||||
break;
|
||||
} elseif ($nextSibling->nodeName === 'img' || $nextSibling->nodeName === 'figure') {
|
||||
// Direct <img> or <figure> without wrapping <p>
|
||||
$newElement->parentNode->removeChild($nextSibling);
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$nextSibling = $nextSibling->nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Modify the DOM by wrapping the <img> tags inside a <figure> and adding the desired structure
|
||||
$crawler->filter('img')->each(function (Crawler $node) {
|
||||
$imgElement = $node->getNode(0);
|
||||
|
||||
// Update the class of the <img>
|
||||
$existingClasses = $imgElement->getAttribute('class');
|
||||
$newClasses = 'img-fluid rounded-2 shadow-sm mb-2';
|
||||
$updatedClasses = ($existingClasses ? $existingClasses.' ' : '').$newClasses;
|
||||
$imgElement->setAttribute('class', $updatedClasses);
|
||||
|
||||
// Create a new <figure> element
|
||||
$figureElement = new \DOMElement('figure');
|
||||
$imgElement->parentNode->insertBefore($figureElement, $imgElement);
|
||||
|
||||
// Move the <img> inside the <figure>
|
||||
$figureElement->appendChild($imgElement);
|
||||
$figureElement->setAttribute('class', 'image');
|
||||
|
||||
// Create a new <footer> element inside <figure>
|
||||
$footerElement = new \DOMElement('footer', $imgElement->getAttribute('alt'));
|
||||
$figureElement->appendChild($footerElement);
|
||||
$footerElement->setAttribute('class', 'image-caption');
|
||||
});
|
||||
|
||||
// Add Bootstrap 5 styling to tables
|
||||
$crawler->filter('table')->each(function (Crawler $node) {
|
||||
$tableElement = $node->getNode(0);
|
||||
$existingClasses = $tableElement->getAttribute('class');
|
||||
$newClass = 'table';
|
||||
$updatedClasses = ($existingClasses ? $existingClasses.' ' : '').$newClass;
|
||||
$tableElement->setAttribute('class', $updatedClasses);
|
||||
});
|
||||
|
||||
// Convert the modified DOM back to HTML
|
||||
$updatedHtml = '';
|
||||
foreach ($crawler as $domElement) {
|
||||
$updatedHtml .= $domElement->ownerDocument->saveHTML($domElement);
|
||||
}
|
||||
|
||||
return $updatedHtml;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
53
app/Models/ShopeeSellerScrape.php
Normal file
53
app/Models/ShopeeSellerScrape.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class ShopeeSellerScrape
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $category_id
|
||||
* @property string $seller
|
||||
* @property string $country_iso
|
||||
* @property int $epoch
|
||||
* @property string $filename
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Category|null $category
|
||||
* @property Collection|ShopeeSellerScrapedImage[] $shopee_seller_scraped_images
|
||||
*/
|
||||
class ShopeeSellerScrape extends Model
|
||||
{
|
||||
protected $table = 'shopee_seller_scrapes';
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'int',
|
||||
'epoch' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'category_id',
|
||||
'seller',
|
||||
'country_iso',
|
||||
'epoch',
|
||||
'filename',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function shopee_seller_scraped_images()
|
||||
{
|
||||
return $this->hasMany(ShopeeSellerScrapedImage::class);
|
||||
}
|
||||
}
|
||||
44
app/Models/ShopeeSellerScrapedImage.php
Normal file
44
app/Models/ShopeeSellerScrapedImage.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class ShopeeSellerScrapedImage
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $shopee_seller_scrape_id
|
||||
* @property string $original_name
|
||||
* @property string|null $image
|
||||
* @property bool $featured
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property ShopeeSellerScrape $shopee_seller_scrape
|
||||
*/
|
||||
class ShopeeSellerScrapedImage extends Model
|
||||
{
|
||||
protected $table = 'shopee_seller_scraped_images';
|
||||
|
||||
protected $casts = [
|
||||
'shopee_seller_scrape_id' => 'int',
|
||||
'featured' => 'bool',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'shopee_seller_scrape_id',
|
||||
'original_name',
|
||||
'image',
|
||||
'featured',
|
||||
];
|
||||
|
||||
public function shopee_seller_scrape()
|
||||
{
|
||||
return $this->belongsTo(ShopeeSellerScrape::class);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,11 @@ public function boot(): void
|
||||
});
|
||||
|
||||
$this->routes(function () {
|
||||
|
||||
Route::middleware('web')
|
||||
->prefix('tests')
|
||||
->group(base_path('routes/tests.php'));
|
||||
|
||||
Route::middleware('api')
|
||||
->prefix('api')
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
"artesaos/seotools": "^1.2",
|
||||
"codex-team/editor.js": "^2.0",
|
||||
"famdirksen/laravel-google-indexing": "^0.5.0",
|
||||
"fivefilters/readability.php": "^1.0",
|
||||
"genealabs/laravel-model-caching": "^0.13.4",
|
||||
"glhd/laravel-timezone-mapper": "^1.4",
|
||||
"graham-campbell/markdown": "^15.0",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"inspector-apm/inspector-laravel": "^4.7",
|
||||
"intervention/image": "^2.7",
|
||||
"kalnoy/nestedset": "^6.0",
|
||||
"laravel-freelancer-nl/laravel-index-now": "^1.2",
|
||||
@@ -25,10 +28,17 @@
|
||||
"laravel/tinker": "^2.8",
|
||||
"laravel/ui": "^4.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"masterminds/html5": "^2.8",
|
||||
"mews/purifier": "^3.4",
|
||||
"pfaciana/tiny-html-minifier": "^3.0",
|
||||
"spatie/browsershot": "^3.58",
|
||||
"spatie/crawler": "^7.1",
|
||||
"spatie/laravel-feed": "^4.2",
|
||||
"spatie/laravel-googletagmanager": "^2.6",
|
||||
"spatie/laravel-sitemap": "^6.3",
|
||||
"stevebauman/location": "^7.0",
|
||||
"symfony/dom-crawler": "^6.3",
|
||||
"thiagoalessio/tesseract_ocr": "^2.12",
|
||||
"tightenco/ziggy": "^1.6"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
1679
composer.lock
generated
1679
composer.lock
generated
File diff suppressed because it is too large
Load Diff
156
config/markdown.php
Normal file
156
config/markdown.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Laravel Markdown.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable View Integration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies if the view integration is enabled so you can write
|
||||
| markdown views and have them rendered as html. The following extensions
|
||||
| are currently supported: ".md", ".md.php", and ".md.blade.php". You may
|
||||
| disable this integration if it is conflicting with another package.
|
||||
|
|
||||
| Default: true
|
||||
|
|
||||
*/
|
||||
|
||||
'views' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CommonMark Extensions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies what extensions will be automatically enabled.
|
||||
| Simply provide your extension class names here.
|
||||
|
|
||||
| Default: [
|
||||
| League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
|
||||
| League\CommonMark\Extension\Table\TableExtension::class,
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'extensions' => [
|
||||
League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
|
||||
League\CommonMark\Extension\Table\TableExtension::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Renderer Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for rendering HTML.
|
||||
|
|
||||
| Default: [
|
||||
| 'block_separator' => "\n",
|
||||
| 'inner_separator' => "\n",
|
||||
| 'soft_break' => "\n",
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'renderer' => [
|
||||
'block_separator' => "\n",
|
||||
'inner_separator' => "\n",
|
||||
'soft_break' => "\n",
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commonmark Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for commonmark.
|
||||
|
|
||||
| Default: [
|
||||
| 'enable_em' => true,
|
||||
| 'enable_strong' => true,
|
||||
| 'use_asterisk' => true,
|
||||
| 'use_underscore' => true,
|
||||
| 'unordered_list_markers' => ['-', '+', '*'],
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'commonmark' => [
|
||||
'enable_em' => true,
|
||||
'enable_strong' => true,
|
||||
'use_asterisk' => true,
|
||||
'use_underscore' => true,
|
||||
'unordered_list_markers' => ['-', '+', '*'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTML Input
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies how to handle untrusted HTML input.
|
||||
|
|
||||
| Default: 'strip'
|
||||
|
|
||||
*/
|
||||
|
||||
'html_input' => 'strip',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allow Unsafe Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies whether to allow risky image URLs and links.
|
||||
|
|
||||
| Default: true
|
||||
|
|
||||
*/
|
||||
|
||||
'allow_unsafe_links' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Nesting Level
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies the maximum permitted block nesting level.
|
||||
|
|
||||
| Default: PHP_INT_MAX
|
||||
|
|
||||
*/
|
||||
|
||||
'max_nesting_level' => PHP_INT_MAX,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Slug Normalizer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for slug normalization.
|
||||
|
|
||||
| Default: [
|
||||
| 'max_length' => 255,
|
||||
| 'unique' => 'document',
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'slug_normalizer' => [
|
||||
'max_length' => 255,
|
||||
'unique' => 'document',
|
||||
],
|
||||
|
||||
];
|
||||
9
config/platform/ai.php
Normal file
9
config/platform/ai.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'openai' => [
|
||||
'api_key' => env('OPENAI_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -6,4 +6,10 @@
|
||||
'dev_default_ip' => env('DEV_DEFAULT_IP', '127.0.0.1'),
|
||||
|
||||
'fallback_country_slug' => 'my',
|
||||
|
||||
'node_binary' => env('NODE_BINARY'),
|
||||
|
||||
'npm_binary' => env('NPM_BINARY'),
|
||||
|
||||
'my_crawler_proxy' => env('MY_CRAWLER_PROXY', 'my.smartproxy.com:30000'),
|
||||
];
|
||||
|
||||
17
config/platform/proxy.php
Normal file
17
config/platform/proxy.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'user_agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
|
||||
|
||||
'smartproxy' => [
|
||||
'rotating_global' => [
|
||||
'user' => 'sp5bbkzj7e',
|
||||
'password' => 'yTtk2cc5kg23kIkSSr',
|
||||
'server' => 'gate.smartproxy.com:7000',
|
||||
'reproxy' => '157.230.194.206:7000',
|
||||
'reproxy_enable' => false,
|
||||
'cost_per_gb' => 7.00,
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -30,6 +30,14 @@
|
||||
|
||||
'connections' => [
|
||||
|
||||
'default' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
32
database/migrations/2023_09_28_183221_create_jobs_table.php
Normal file
32
database/migrations/2023_09_28_183221_create_jobs_table.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shopee_seller_scrapes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('category_id');
|
||||
$table->string('seller');
|
||||
$table->string('country_iso');
|
||||
$table->bigInteger('epoch');
|
||||
$table->string('filename');
|
||||
$table->timestamp('last_ai_written_at');
|
||||
$table->integer('write_counts')->default(0);
|
||||
$table->timestamps();
|
||||
$table->foreign('category')->references('id')->on('categories');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shopee_seller_scrapes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shopee_seller_scraped_images', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('shopee_seller_scrape_id');
|
||||
$table->string('original_name');
|
||||
$table->string('image')->nullable();
|
||||
$table->boolean('featured')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('shopee_seller_scrape_id')->references('id')->on('shopee_seller_scrapes');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shopee_seller_scraped_images');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ai_writeups', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('source');
|
||||
$table->string('source_url');
|
||||
$table->foreignId('category_id');
|
||||
$table->string('title');
|
||||
$table->string('editor_format');
|
||||
$table->mediumText('excerpt')->nullable();
|
||||
$table->string('featured_image')->nullable();
|
||||
$table->json('body')->nullable();
|
||||
$table->double('cost', 5, 5);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ai_writeups');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE posts CHANGE COLUMN editor editor ENUM('editorjs', 'markdown') NOT NULL DEFAULT 'editorjs'");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE posts CHANGE COLUMN editor editor ENUM('editorjs') NOT NULL DEFAULT 'editorjs'");
|
||||
}
|
||||
};
|
||||
18
dev.sh
Normal file
18
dev.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
eval 'php artisan optimize:clear';
|
||||
# eval 'php artisan responsecache:clear';
|
||||
# eval 'php artisan opcache:clear';
|
||||
eval 'php artisan ziggy:generate';
|
||||
eval 'blade-formatter --write resources/**/*.blade.php';
|
||||
eval './vendor/bin/pint';
|
||||
# eval 'npm run dev';
|
||||
|
||||
tmux \
|
||||
new-session 'npm run dev' \; \
|
||||
# split-window 'php artisan queue:work' \; \
|
||||
# split-window 'php artisan schedule:work' \; \
|
||||
# split-window 'php artisan horizon' \; \
|
||||
# new-window \; \
|
||||
# detach-client
|
||||
tmux a
|
||||
1764
package-lock.json
generated
1764
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^2.1.6",
|
||||
"puppeteer": "^21.3.5",
|
||||
"vue": "^3.3.4",
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-loader": "^17.2.2",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
1
public/build/assets/NativeImageBlock-78162560.js
Normal file
1
public/build/assets/NativeImageBlock-78162560.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/NativeImageBlock-78162560.js.gz
Normal file
BIN
public/build/assets/NativeImageBlock-78162560.js.gz
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
import Qn from"./VueEditorJs-fc69dbb5.js";import{r as Ft,_ as Mr}from"./NativeImageBlock-610fd8da.js";import{L as hn}from"./bundle-dbffa4bb.js";import{H as yn}from"./bundle-7b5ccf90.js";import{g as Cr,d as Pr,a as ua,r as zt,b as ne,c as vt,u as nn,t as da,o as ct,e as rn,w as Nt,f as Z,h as R,i as Q,j as _t,k as nt,l as Fe,m as _e,n as ie,p as ze,q as ft,s as j,v as Qe,x as gn,y as Pe,z as G,A as Gn,T as Sr,B as Ce,C as he,D as J,E as ot,F as we,G as It,H as rt,I as Ve,J as Zt,K as At,L as yt,M as wa,N as Or,O as Nr,P as Ar,_ as $r,Q as Ir,R as Er,S as Yr,U as Ia,V as Ur,W as Lr,X as wn}from"./admin-app-c0ef582d.js";import"./index-8746c87e.js";var Xn={exports:{}};/*!
|
||||
import Qn from"./VueEditorJs-8bfa8291.js";import{r as Ft,_ as Mr}from"./NativeImageBlock-78162560.js";import{L as hn}from"./bundle-13ffaba5.js";import{H as yn}from"./bundle-9b767e03.js";import{g as Cr,d as Pr,a as ua,r as zt,b as ne,c as vt,u as nn,t as da,o as ct,e as rn,w as Nt,f as Z,h as R,i as Q,j as _t,k as nt,l as Fe,m as _e,n as ie,p as ze,q as ft,s as j,v as Qe,x as gn,y as Pe,z as G,A as Gn,T as Sr,B as Ce,C as he,D as J,E as ot,F as we,G as It,H as rt,I as Ve,J as Zt,K as At,L as yt,M as wa,N as Or,O as Nr,P as Ar,_ as $r,Q as Ir,R as Er,S as Yr,U as Ia,V as Ur,W as Lr,X as wn}from"./admin-app-62da08c5.js";import"./index-8746c87e.js";var Xn={exports:{}};/*!
|
||||
* Image tool
|
||||
*
|
||||
* @version 2.8.1
|
||||
BIN
public/build/assets/PostEditor-5f10a2ff.js.gz
Normal file
BIN
public/build/assets/PostEditor-5f10a2ff.js.gz
Normal file
Binary file not shown.
Binary file not shown.
83
public/build/assets/VueEditorJs-8bfa8291.js
Normal file
83
public/build/assets/VueEditorJs-8bfa8291.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/VueEditorJs-8bfa8291.js.gz
Normal file
BIN
public/build/assets/VueEditorJs-8bfa8291.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
19
public/build/assets/admin-app-62da08c5.js
Normal file
19
public/build/assets/admin-app-62da08c5.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/admin-app-62da08c5.js.gz
Normal file
BIN
public/build/assets/admin-app-62da08c5.js.gz
Normal file
Binary file not shown.
1
public/build/assets/admin-app-6630652e.css
Normal file
1
public/build/assets/admin-app-6630652e.css
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/admin-app-6630652e.css.gz
Normal file
BIN
public/build/assets/admin-app-6630652e.css.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
public/build/assets/bootstrap-icons-4d4572ef.woff
Normal file
BIN
public/build/assets/bootstrap-icons-4d4572ef.woff
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/build/assets/bootstrap-icons-bacd70af.woff2
Normal file
BIN
public/build/assets/bootstrap-icons-bacd70af.woff2
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
import{g as E}from"./admin-app-c0ef582d.js";function P(_,j){for(var v=0;v<j.length;v++){const p=j[v];if(typeof p!="string"&&!Array.isArray(p)){for(const c in p)if(c!=="default"&&!(c in _)){const o=Object.getOwnPropertyDescriptor(p,c);o&&Object.defineProperty(_,c,o.get?o:{enumerable:!0,get:()=>p[c]})}}}return Object.freeze(Object.defineProperty(_,Symbol.toStringTag,{value:"Module"}))}var T={exports:{}};(function(_,j){(function(v,p){_.exports=p()})(window,function(){return function(v){var p={};function c(o){if(p[o])return p[o].exports;var l=p[o]={i:o,l:!1,exports:{}};return v[o].call(l.exports,l,l.exports,c),l.l=!0,l.exports}return c.m=v,c.c=p,c.d=function(o,l,d){c.o(o,l)||Object.defineProperty(o,l,{enumerable:!0,get:d})},c.r=function(o){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})},c.t=function(o,l){if(1&l&&(o=c(o)),8&l||4&l&&typeof o=="object"&&o&&o.__esModule)return o;var d=Object.create(null);if(c.r(d),Object.defineProperty(d,"default",{enumerable:!0,value:o}),2&l&&typeof o!="string")for(var f in o)c.d(d,f,(function(b){return o[b]}).bind(null,f));return d},c.n=function(o){var l=o&&o.__esModule?function(){return o.default}:function(){return o};return c.d(l,"a",l),l},c.o=function(o,l){return Object.prototype.hasOwnProperty.call(o,l)},c.p="/",c(c.s=4)}([function(v,p,c){var o=c(1),l=c(2);typeof(l=l.__esModule?l.default:l)=="string"&&(l=[[v.i,l,""]]);var d={insert:"head",singleton:!1};o(l,d),v.exports=l.locals||{}},function(v,p,c){var o,l=function(){return o===void 0&&(o=!!(window&&document&&document.all&&!window.atob)),o},d=function(){var r={};return function(i){if(r[i]===void 0){var s=document.querySelector(i);if(window.HTMLIFrameElement&&s instanceof window.HTMLIFrameElement)try{s=s.contentDocument.head}catch{s=null}r[i]=s}return r[i]}}(),f=[];function b(r){for(var i=-1,s=0;s<f.length;s++)if(f[s].identifier===r){i=s;break}return i}function S(r,i){for(var s={},u=[],m=0;m<r.length;m++){var g=r[m],y=i.base?g[0]+i.base:g[0],C=s[y]||0,O="".concat(y," ").concat(C);s[y]=C+1;var L=b(O),M={css:g[1],media:g[2],sourceMap:g[3]};L!==-1?(f[L].references++,f[L].updater(M)):f.push({identifier:O,updater:h(M,i),references:1}),u.push(O)}return u}function k(r){var i=document.createElement("style"),s=r.attributes||{};if(s.nonce===void 0){var u=c.nc;u&&(s.nonce=u)}if(Object.keys(s).forEach(function(g){i.setAttribute(g,s[g])}),typeof r.insert=="function")r.insert(i);else{var m=d(r.insert||"head");if(!m)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");m.appendChild(i)}return i}var w,x=(w=[],function(r,i){return w[r]=i,w.filter(Boolean).join(`
|
||||
import{g as E}from"./admin-app-62da08c5.js";function P(_,j){for(var v=0;v<j.length;v++){const p=j[v];if(typeof p!="string"&&!Array.isArray(p)){for(const c in p)if(c!=="default"&&!(c in _)){const o=Object.getOwnPropertyDescriptor(p,c);o&&Object.defineProperty(_,c,o.get?o:{enumerable:!0,get:()=>p[c]})}}}return Object.freeze(Object.defineProperty(_,Symbol.toStringTag,{value:"Module"}))}var T={exports:{}};(function(_,j){(function(v,p){_.exports=p()})(window,function(){return function(v){var p={};function c(o){if(p[o])return p[o].exports;var l=p[o]={i:o,l:!1,exports:{}};return v[o].call(l.exports,l,l.exports,c),l.l=!0,l.exports}return c.m=v,c.c=p,c.d=function(o,l,d){c.o(o,l)||Object.defineProperty(o,l,{enumerable:!0,get:d})},c.r=function(o){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(o,"__esModule",{value:!0})},c.t=function(o,l){if(1&l&&(o=c(o)),8&l||4&l&&typeof o=="object"&&o&&o.__esModule)return o;var d=Object.create(null);if(c.r(d),Object.defineProperty(d,"default",{enumerable:!0,value:o}),2&l&&typeof o!="string")for(var f in o)c.d(d,f,(function(b){return o[b]}).bind(null,f));return d},c.n=function(o){var l=o&&o.__esModule?function(){return o.default}:function(){return o};return c.d(l,"a",l),l},c.o=function(o,l){return Object.prototype.hasOwnProperty.call(o,l)},c.p="/",c(c.s=4)}([function(v,p,c){var o=c(1),l=c(2);typeof(l=l.__esModule?l.default:l)=="string"&&(l=[[v.i,l,""]]);var d={insert:"head",singleton:!1};o(l,d),v.exports=l.locals||{}},function(v,p,c){var o,l=function(){return o===void 0&&(o=!!(window&&document&&document.all&&!window.atob)),o},d=function(){var r={};return function(i){if(r[i]===void 0){var s=document.querySelector(i);if(window.HTMLIFrameElement&&s instanceof window.HTMLIFrameElement)try{s=s.contentDocument.head}catch{s=null}r[i]=s}return r[i]}}(),f=[];function b(r){for(var i=-1,s=0;s<f.length;s++)if(f[s].identifier===r){i=s;break}return i}function S(r,i){for(var s={},u=[],m=0;m<r.length;m++){var g=r[m],y=i.base?g[0]+i.base:g[0],C=s[y]||0,O="".concat(y," ").concat(C);s[y]=C+1;var L=b(O),M={css:g[1],media:g[2],sourceMap:g[3]};L!==-1?(f[L].references++,f[L].updater(M)):f.push({identifier:O,updater:h(M,i),references:1}),u.push(O)}return u}function k(r){var i=document.createElement("style"),s=r.attributes||{};if(s.nonce===void 0){var u=c.nc;u&&(s.nonce=u)}if(Object.keys(s).forEach(function(g){i.setAttribute(g,s[g])}),typeof r.insert=="function")r.insert(i);else{var m=d(r.insert||"head");if(!m)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");m.appendChild(i)}return i}var w,x=(w=[],function(r,i){return w[r]=i,w.filter(Boolean).join(`
|
||||
`)});function a(r,i,s,u){var m=s?"":u.media?"@media ".concat(u.media," {").concat(u.css,"}"):u.css;if(r.styleSheet)r.styleSheet.cssText=x(i,m);else{var g=document.createTextNode(m),y=r.childNodes;y[i]&&r.removeChild(y[i]),y.length?r.insertBefore(g,y[i]):r.appendChild(g)}}function e(r,i,s){var u=s.css,m=s.media,g=s.sourceMap;if(m?r.setAttribute("media",m):r.removeAttribute("media"),g&&btoa&&(u+=`
|
||||
/*# sourceMappingURL=data:application/json;base64,`.concat(btoa(unescape(encodeURIComponent(JSON.stringify(g))))," */")),r.styleSheet)r.styleSheet.cssText=u;else{for(;r.firstChild;)r.removeChild(r.firstChild);r.appendChild(document.createTextNode(u))}}var t=null,n=0;function h(r,i){var s,u,m;if(i.singleton){var g=n++;s=t||(t=k(i)),u=a.bind(null,s,g,!1),m=a.bind(null,s,g,!0)}else s=k(i),u=e.bind(null,s,i),m=function(){(function(y){if(y.parentNode===null)return!1;y.parentNode.removeChild(y)})(s)};return u(r),function(y){if(y){if(y.css===r.css&&y.media===r.media&&y.sourceMap===r.sourceMap)return;u(r=y)}else m()}}v.exports=function(r,i){(i=i||{}).singleton||typeof i.singleton=="boolean"||(i.singleton=l());var s=S(r=r||[],i);return function(u){if(u=u||[],Object.prototype.toString.call(u)==="[object Array]"){for(var m=0;m<s.length;m++){var g=b(s[m]);f[g].references--}for(var y=S(u,i),C=0;C<s.length;C++){var O=b(s[C]);f[O].references===0&&(f[O].updater(),f.splice(O,1))}s=y}}}},function(v,p,c){(p=c(3)(!1)).push([v.i,`.cdx-list {
|
||||
margin: 0;
|
||||
BIN
public/build/assets/bundle-13ffaba5.js.gz
Normal file
BIN
public/build/assets/bundle-13ffaba5.js.gz
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
import{g as N}from"./admin-app-c0ef582d.js";function P(x,H){for(var g=0;g<H.length;g++){const b=H[g];if(typeof b!="string"&&!Array.isArray(b)){for(const l in b)if(l!=="default"&&!(l in x)){const n=Object.getOwnPropertyDescriptor(b,l);n&&Object.defineProperty(x,l,n.get?n:{enumerable:!0,get:()=>b[l]})}}}return Object.freeze(Object.defineProperty(x,Symbol.toStringTag,{value:"Module"}))}var E={exports:{}};(function(x,H){(function(g,b){x.exports=b()})(window,function(){return function(g){var b={};function l(n){if(b[n])return b[n].exports;var i=b[n]={i:n,l:!1,exports:{}};return g[n].call(i.exports,i,i.exports,l),i.l=!0,i.exports}return l.m=g,l.c=b,l.d=function(n,i,h){l.o(n,i)||Object.defineProperty(n,i,{enumerable:!0,get:h})},l.r=function(n){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},l.t=function(n,i){if(1&i&&(n=l(n)),8&i||4&i&&typeof n=="object"&&n&&n.__esModule)return n;var h=Object.create(null);if(l.r(h),Object.defineProperty(h,"default",{enumerable:!0,value:n}),2&i&&typeof n!="string")for(var m in n)l.d(h,m,(function(f){return n[f]}).bind(null,m));return h},l.n=function(n){var i=n&&n.__esModule?function(){return n.default}:function(){return n};return l.d(i,"a",i),i},l.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},l.p="/",l(l.s=5)}([function(g,b,l){var n=l(1);typeof n=="string"&&(n=[[g.i,n,""]]);var i={hmr:!0,transform:void 0,insertInto:void 0};l(3)(n,i),n.locals&&(g.exports=n.locals)},function(g,b,l){(g.exports=l(2)(!1)).push([g.i,`/**
|
||||
import{g as N}from"./admin-app-62da08c5.js";function P(x,H){for(var g=0;g<H.length;g++){const b=H[g];if(typeof b!="string"&&!Array.isArray(b)){for(const l in b)if(l!=="default"&&!(l in x)){const n=Object.getOwnPropertyDescriptor(b,l);n&&Object.defineProperty(x,l,n.get?n:{enumerable:!0,get:()=>b[l]})}}}return Object.freeze(Object.defineProperty(x,Symbol.toStringTag,{value:"Module"}))}var E={exports:{}};(function(x,H){(function(g,b){x.exports=b()})(window,function(){return function(g){var b={};function l(n){if(b[n])return b[n].exports;var i=b[n]={i:n,l:!1,exports:{}};return g[n].call(i.exports,i,i.exports,l),i.l=!0,i.exports}return l.m=g,l.c=b,l.d=function(n,i,h){l.o(n,i)||Object.defineProperty(n,i,{enumerable:!0,get:h})},l.r=function(n){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},l.t=function(n,i){if(1&i&&(n=l(n)),8&i||4&i&&typeof n=="object"&&n&&n.__esModule)return n;var h=Object.create(null);if(l.r(h),Object.defineProperty(h,"default",{enumerable:!0,value:n}),2&i&&typeof n!="string")for(var m in n)l.d(h,m,(function(f){return n[f]}).bind(null,m));return h},l.n=function(n){var i=n&&n.__esModule?function(){return n.default}:function(){return n};return l.d(i,"a",i),i},l.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},l.p="/",l(l.s=5)}([function(g,b,l){var n=l(1);typeof n=="string"&&(n=[[g.i,n,""]]);var i={hmr:!0,transform:void 0,insertInto:void 0};l(3)(n,i),n.locals&&(g.exports=n.locals)},function(g,b,l){(g.exports=l(2)(!1)).push([g.i,`/**
|
||||
* Plugin styles
|
||||
*/
|
||||
.ce-header {
|
||||
BIN
public/build/assets/bundle-9b767e03.js.gz
Normal file
BIN
public/build/assets/bundle-9b767e03.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
public/build/assets/front-app-b716c47a.js.gz
Normal file
BIN
public/build/assets/front-app-b716c47a.js.gz
Normal file
Binary file not shown.
9
public/build/assets/front-app-f0b54e22.css
Normal file
9
public/build/assets/front-app-f0b54e22.css
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/front-app-f0b54e22.css.gz
Normal file
BIN
public/build/assets/front-app-f0b54e22.css.gz
Normal file
Binary file not shown.
@@ -3,25 +3,25 @@
|
||||
"file": "assets/NativeImageBlock-e3b0c442.css",
|
||||
"src": "NativeImageBlock.css"
|
||||
},
|
||||
"_NativeImageBlock-610fd8da.js": {
|
||||
"_NativeImageBlock-78162560.js": {
|
||||
"css": [
|
||||
"assets/NativeImageBlock-e3b0c442.css"
|
||||
],
|
||||
"file": "assets/NativeImageBlock-610fd8da.js",
|
||||
"file": "assets/NativeImageBlock-78162560.js",
|
||||
"imports": [
|
||||
"resources/js/admin-app.js"
|
||||
],
|
||||
"isDynamicEntry": true
|
||||
},
|
||||
"_bundle-7b5ccf90.js": {
|
||||
"file": "assets/bundle-7b5ccf90.js",
|
||||
"_bundle-13ffaba5.js": {
|
||||
"file": "assets/bundle-13ffaba5.js",
|
||||
"imports": [
|
||||
"resources/js/admin-app.js"
|
||||
],
|
||||
"isDynamicEntry": true
|
||||
},
|
||||
"_bundle-dbffa4bb.js": {
|
||||
"file": "assets/bundle-dbffa4bb.js",
|
||||
"_bundle-9b767e03.js": {
|
||||
"file": "assets/bundle-9b767e03.js",
|
||||
"imports": [
|
||||
"resources/js/admin-app.js"
|
||||
],
|
||||
@@ -31,11 +31,11 @@
|
||||
"file": "assets/index-8746c87e.js"
|
||||
},
|
||||
"node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff": {
|
||||
"file": "assets/bootstrap-icons-999550fa.woff",
|
||||
"file": "assets/bootstrap-icons-4d4572ef.woff",
|
||||
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff"
|
||||
},
|
||||
"node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2": {
|
||||
"file": "assets/bootstrap-icons-cfe45b98.woff2",
|
||||
"file": "assets/bootstrap-icons-bacd70af.woff2",
|
||||
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2"
|
||||
},
|
||||
"resources/js/admin-app.css": {
|
||||
@@ -47,11 +47,11 @@
|
||||
"assets/admin-app-935fc652.css"
|
||||
],
|
||||
"dynamicImports": [
|
||||
"_NativeImageBlock-610fd8da.js",
|
||||
"_NativeImageBlock-78162560.js",
|
||||
"resources/js/vue/PostEditor.vue",
|
||||
"resources/js/vue/VueEditorJs.vue"
|
||||
],
|
||||
"file": "assets/admin-app-c0ef582d.js",
|
||||
"file": "assets/admin-app-62da08c5.js",
|
||||
"imports": [
|
||||
"_index-8746c87e.js"
|
||||
],
|
||||
@@ -59,7 +59,7 @@
|
||||
"src": "resources/js/admin-app.js"
|
||||
},
|
||||
"resources/js/front-app.js": {
|
||||
"file": "assets/front-app-0cdc6a38.js",
|
||||
"file": "assets/front-app-b716c47a.js",
|
||||
"imports": [
|
||||
"_index-8746c87e.js"
|
||||
],
|
||||
@@ -74,12 +74,12 @@
|
||||
"css": [
|
||||
"assets/PostEditor-8d534a4a.css"
|
||||
],
|
||||
"file": "assets/PostEditor-8fdf28c3.js",
|
||||
"file": "assets/PostEditor-5f10a2ff.js",
|
||||
"imports": [
|
||||
"resources/js/vue/VueEditorJs.vue",
|
||||
"_NativeImageBlock-610fd8da.js",
|
||||
"_bundle-dbffa4bb.js",
|
||||
"_bundle-7b5ccf90.js",
|
||||
"_NativeImageBlock-78162560.js",
|
||||
"_bundle-13ffaba5.js",
|
||||
"_bundle-9b767e03.js",
|
||||
"resources/js/admin-app.js",
|
||||
"_index-8746c87e.js"
|
||||
],
|
||||
@@ -88,10 +88,10 @@
|
||||
},
|
||||
"resources/js/vue/VueEditorJs.vue": {
|
||||
"dynamicImports": [
|
||||
"_bundle-7b5ccf90.js",
|
||||
"_bundle-dbffa4bb.js"
|
||||
"_bundle-9b767e03.js",
|
||||
"_bundle-13ffaba5.js"
|
||||
],
|
||||
"file": "assets/VueEditorJs-fc69dbb5.js",
|
||||
"file": "assets/VueEditorJs-8bfa8291.js",
|
||||
"imports": [
|
||||
"resources/js/admin-app.js",
|
||||
"_index-8746c87e.js"
|
||||
@@ -100,12 +100,12 @@
|
||||
"src": "resources/js/vue/VueEditorJs.vue"
|
||||
},
|
||||
"resources/sass/admin-app.scss": {
|
||||
"file": "assets/admin-app-bade20ce.css",
|
||||
"file": "assets/admin-app-6630652e.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/sass/admin-app.scss"
|
||||
},
|
||||
"resources/sass/front-app.scss": {
|
||||
"file": "assets/front-app-1a35e3f2.css",
|
||||
"file": "assets/front-app-f0b54e22.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/sass/front-app.scss"
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -92,8 +92,13 @@ class="rounded-3 d-flex justify-content-center">
|
||||
<a href="{{ route('posts.manage.indexing', ['post_id' => $post->id]) }}"
|
||||
class="btn">Index to Search Engines</a>
|
||||
@endif
|
||||
<a href="{{ route('posts.manage.edit', ['post_id' => $post->id]) }}"
|
||||
class="btn">Edit</a>
|
||||
|
||||
@if ($post->editor == 'editorjs')
|
||||
<a href="{{ route('posts.manage.edit', ['post_id' => $post->id]) }}"
|
||||
class="btn">Edit</a>
|
||||
@else
|
||||
<a href="#" class="btn disabled">Edit (Disabled)</a>
|
||||
@endif
|
||||
@if ($post->status == 'trash')
|
||||
<a href="{{ route('posts.manage.delete', ['post_id' => $post->id]) }}"
|
||||
class="btn">Delete Forever</a>
|
||||
|
||||
@@ -38,13 +38,13 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="img-fluid rounded-start ratio ratio-16x9">
|
||||
<div class="lqip-loader">
|
||||
<div class="lqip-loader rounded-2 shadow-sm">
|
||||
<!-- Use the LQIP image with the appropriate URL -->
|
||||
<img class="img-fluid rounded-2" src="{{ $post->featured_image }}"
|
||||
<img class="img-fluid" src="{{ $post->featured_image }}"
|
||||
alt="Photo of {{ $post->name }}">
|
||||
|
||||
<!-- Use the final JPEG image with the appropriate URL -->
|
||||
<img class="lqip-frozen img-fluid rounded-2" src="{{ $post->featured_image_lqip }}"
|
||||
<img class="lqip-frozen img-fluid" src="{{ $post->featured_image_lqip }}"
|
||||
alt="Placeholder image of {{ $post->name }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
7
routes/tests.php
Normal file
7
routes/tests.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/scrape', [App\Http\Controllers\Tests\ScraperTestController::class, 'scrape']);
|
||||
|
||||
Route::get('/gen', [App\Http\Controllers\Tests\ScraperTestController::class, 'gen']);
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
eval 'php artisan ziggy:generate';
|
||||
eval 'npm run dev';
|
||||
Reference in New Issue
Block a user