Track Filament

This commit is contained in:
Hickmeister
2025-01-05 02:28:51 +00:00
parent e31bd43eaa
commit 5a11305caf
7 changed files with 1664 additions and 2 deletions

View File

@@ -1,5 +1,7 @@
{ {
"require": { "require": {
"php-mqtt/client": "^2.2" "php-mqtt/client": "^2.2",
"fabpot/goutte": "^4.0",
"symfony/polyfill-ctype": "^1.31"
} }
} }

1108
composer.lock generated

File diff suppressed because it is too large Load Diff

194
public/addFilament.php Normal file
View File

@@ -0,0 +1,194 @@
<?php include '../src/session_check.php'; ?>
<html lang="en" data-bs-theme="light">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--favicon-->
<link rel="icon" href="../assets/images/favicon-32x32.png" type="image/png">
<!--plugins-->
<link href="../assets/plugins/vectormap/jquery-jvectormap-2.0.2.css" rel="stylesheet">
<link href="../assets/plugins/simplebar/css/simplebar.css" rel="stylesheet">
<link href="../assets/plugins/perfect-scrollbar/css/perfect-scrollbar.css" rel="stylesheet">
<link href="../assets/plugins/metismenu/css/metisMenu.min.css" rel="stylesheet">
<!-- loader-->
<link href="../assets/css/pace.min.css" rel="stylesheet"/>
<script src="../assets/js/pace.min.js"></script>
<!-- Bootstrap CSS -->
<link href="../assets/css/bootstrap.min.css" rel="stylesheet">
<link href="../assets/css/bootstrap-extended.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
<link href="../assets/sass/app.css" rel="stylesheet">
<link href="../assets/css/icons.css" rel="stylesheet">
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
<!-- Theme Style CSS -->
<link rel="stylesheet" href="../assets/sass/dark-theme.css">
<link rel="stylesheet" href="../assets/sass/semi-dark.css">
<link rel="stylesheet" href="../assets/sass/bordered-theme.css">
<title>TOD Dashboard</title>
</head>
<body>
<!--wrapper-->
<div class="wrapper">
<!--sidebar wrapper -->
<?php include '../src/nav.php'; ?>
<!--end sidebar wrapper -->
<!--start header -->
<?php include '../src/header.php'; ?>
<!--end header -->
<!--start page wrapper -->
<div class="page-wrapper">
<div class="page-content">
<!--start page content -->
<!--breadcrumb-->
<div class="page-breadcrumb d-none d-sm-flex align-items-center mb-3">
<div class="breadcrumb-title pe-3">Filament Settings</div>
<div class="ps-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 p-0">
<li class="breadcrumb-item"><a href="index.php"><i class="bx bx-home-alt"></i></a>
</li>
<li class="breadcrumb-item active" aria-current="page">Add Filament</li>
</ol>
</nav>
</div>
</div>
<!--end breadcrumb-->
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header px-4 py-3">
<h5 class="mb-0">Add New Filament to Track</h5>
</div>
<div class="card-body p-4">
<!-- Alert Banner (Hidden Initially) -->
<div id="alertBanner" class="alert d-none" role="alert"></div>
<form id="filamentForm">
<div class="row mb-3">
<label for="productName" class="col-sm-3 col-form-label">Filament Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="productName" name="productName" placeholder="Brand / Type of Filament" required>
</div>
</div>
<div class="row mb-3">
<label for="productUrl" class="col-sm-3 col-form-label">Amazon URL</label>
<div class="col-sm-9">
<input type="url" class="form-control" id="productUrl" name="productUrl" placeholder="https://www.amazon.co.uk/example" required>
</div>
</div>
<div class="row">
<label class="col-sm-3 col-form-label"></label>
<div class="col-sm-9">
<div class="d-md-flex d-grid align-items-center gap-3">
<button type="submit" class="btn btn-primary px-4">Track Filament</button>
<button type="reset" class="btn btn-light px-4">Reset</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- jQuery & AJAX Script -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
$('#filamentForm').on('submit', function(event) {
event.preventDefault(); // Prevent traditional form submission
// Collect form data
let formData = $(this).serialize();
// AJAX request to submit form
$.ajax({
type: 'POST',
url: '../src/filamentTracker/addFilament.php',
data: formData,
dataType: 'json',
success: function(response) {
let banner = $('#alertBanner');
if (response.status === 'success') {
banner.removeClass('d-none alert-danger')
.addClass('alert-success')
.text(response.message);
} else {
banner.removeClass('d-none alert-success')
.addClass('alert-danger')
.text(response.message);
}
},
error: function() {
$('#alertBanner').removeClass('d-none alert-success')
.addClass('alert-danger')
.text('An error occurred while adding the filament.');
}
});
});
});
</script>
<!--end page content -->
</div>
</div>
<!--end page wrapper -->
<!--start overlay-->
<div class="overlay mobile-toggle-icon"></div>
<!--end overlay-->
<!--Start Back To Top Button-->
<a href="javaScript:;" class="back-to-top"><i class='bx bxs-up-arrow-alt'></i></a>
<!--End Back To Top Button-->
<footer class="page-footer">
<p class="mb-0">Copyright © 2024. All right reserved.</p>
</footer>
</div>
<!--end wrapper-->
<!-- search modal -->
<div class="modal" id="SearchModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-fullscreen-md-down">
<div class="modal-content">
<div class="modal-header gap-2">
<div class="position-relative popup-search w-100">
<input class="form-control form-control-lg ps-5 border border-3 border-primary" type="search" placeholder="Search">
<span class="position-absolute top-50 search-show ms-3 translate-middle-y start-0 top-50 fs-4"><i class='bx bx-search'></i></span>
</div>
<button type="button" class="btn-close d-md-none" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- end search modal -->
<!-- Bootstrap JS -->
<script src="../assets/js/bootstrap.bundle.min.js"></script>
<!--plugins-->
<script src="../assets/js/jquery.min.js"></script>
<script src="../assets/plugins/simplebar/js/simplebar.min.js"></script>
<script src="../assets/plugins/metismenu/js/metisMenu.min.js"></script>
<script src="../assets/plugins/perfect-scrollbar/js/perfect-scrollbar.js"></script>
<script src="../assets/plugins/apexcharts-bundle/js/apexcharts.min.js"></script>
<!--app JS-->
<script src="../assets/js/app.js"></script>
<script src="../assets/js/index.js"></script>
<script src="../assets/plugins/peity/jquery.peity.min.js"></script>
<script>
$(".data-attributes span").peity("donut")
</script>
</body>
</html>

View File

@@ -0,0 +1,131 @@
<?php
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
use Goutte\Client;
// Start session to get user ID
session_start();
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
// Check for POST data
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filamentName = $_POST['productName'] ?? '';
$amazonUrl = $_POST['productUrl'] ?? '';
$userId = $_SESSION['userId'];
// Basic validation
if (empty($filamentName) || empty($amazonUrl)) {
echo json_encode(['status' => 'error', 'message' => 'Filament name and URL are required.']);
exit;
}
$client = new Client();
try {
// Scrape the Amazon page for initial price and other details
$crawler = $client->request('GET', $amazonUrl);
// Scrape Price
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0';
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->filter('.a-price-fraction')->text() : '00';
$whole = preg_replace('/[^0-9]/', '', $whole);
$fraction = preg_replace('/[^0-9]/', '', $fraction);
$fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2);
$totalPrice = floatval($whole . '.' . $fraction);
// Scrape Filament Details from Table
$details = [];
$crawler->filter('table.a-normal tr')->each(function ($node) use (&$details) {
$label = trim($node->filter('td.a-span3')->text());
$value = trim($node->filter('td.a-span9')->text());
$details[$label] = $value;
});
// Extract details or use default if not found
$brand = $details['Brand'] ?? 'Unknown';
$material = $details['Material'] ?? 'Unknown';
$color = $details['Colour'] ?? 'Unknown';
$filamentWeight = isset($details['Item weight']) ? preg_replace('/[^0-9.]/', '', $details['Item weight']) : 1;
if (stripos($details['Item weight'], 'kilograms') !== false || stripos($details['Item weight'], 'kg') !== false) {
$filamentWeight = $filamentWeight * 1000; // Convert KG to G
}
$itemDiameter = isset($details['Item diameter']) ? preg_replace('/[^0-9.]/', '', $details['Item diameter']) : 1.75;
// Start a transaction to ensure consistency
$pdo->beginTransaction();
// Check if filament already exists
$stmt = $pdo->prepare("SELECT id FROM filamentTracker WHERE amazonUrl = :amazonUrl");
$stmt->execute([':amazonUrl' => $amazonUrl]);
$existingFilament = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingFilament) {
// Filament exists just update the price history
$filamentId = $existingFilament['id'];
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
]);
$message = 'Filament price updated successfully.';
} else {
// Insert new filament into filamentTracker
$stmt = $pdo->prepare("
INSERT INTO filamentTracker (userId, filamentName, amazonUrl, filamentWeight, brand, material, color, itemDiameter)
VALUES (:userId, :filamentName, :amazonUrl, :filamentWeight, :brand, :material, :color, :itemDiameter)
");
$stmt->execute([
':userId' => $userId,
':filamentName' => $filamentName,
':amazonUrl' => $amazonUrl,
':filamentWeight' => $filamentWeight,
':brand' => $brand,
':material' => $material,
':color' => $color,
':itemDiameter' => $itemDiameter
]);
// Get the last inserted filament ID
$filamentId = $pdo->lastInsertId();
// Insert initial price into filamentPriceHistory
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
]);
$message = $filamentName . ' Filament added successfully and price tracked.';
}
// Commit the transaction
$pdo->commit();
echo json_encode(['status' => 'success', 'message' => $message]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(['status' => 'error', 'message' => 'Failed to scrape or insert data. Error: ' . $e->getMessage()]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method.']);
}
?>

View File

@@ -0,0 +1,68 @@
<?php
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
// Check if the user is logged in
if (!isset($_SESSION['userId'])) {
echo json_encode(['status' => 'error', 'message' => 'User not authenticated.']);
exit;
}
header('Content-Type: application/json');
// Fetch all filament prices from the database
try {
$stmt = $pdo->query("
SELECT
ft.filamentName,
ft.brand,
ft.material,
ft.color,
fp.price,
fp.recordedAt
FROM
filamentTracker ft
JOIN
filamentPriceHistory fp ON ft.id = fp.filamentId
ORDER BY
ft.filamentName,
fp.recordedAt ASC
");
$filaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
// Format data for charts (grouped by filament)
foreach ($filaments as $filament) {
$name = $filament['filamentName'];
if (!isset($result[$name])) {
$result[$name] = [
'brand' => $filament['brand'],
'material' => $filament['material'],
'color' => $filament['color'],
'prices' => []
];
}
$result[$name]['prices'][] = [
'price' => $filament['price'],
'recordedAt' => $filament['recordedAt']
];
}
// Return as JSON
echo json_encode([
'status' => 'success',
'data' => $result
], JSON_PRETTY_PRINT);
} catch (PDOException $e) {
echo json_encode([
'status' => 'error',
'message' => 'Failed to fetch filament prices: ' . $e->getMessage()
]);
}
?>

View File

@@ -0,0 +1,98 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require '../../vendor/autoload.php';
require '../db.php';
require_once '../envLoader.php';
loadEnv(__DIR__ . '/../../.env');
use Goutte\Client;
function scrapeFilamentPrice($productName, $productUrl) {
$client = new Client();
$crawler = $client->request('GET', $productUrl);
// Extract the current price
$price = $crawler->filter('span.a-price-whole')->first()->text();
$price = str_replace(',', '', $price); // Clean the price string
// Extract the original price (if available)
$originalPriceNode = $crawler->filter('span.a-price.a-text-price')->first();
$originalPrice = $originalPriceNode->count() ? str_replace(',', '', $originalPriceNode->text()) : null;
// Calculate the discount percentage
$discountPercentage = null;
if ($originalPrice) {
$discountPercentage = round(100 - (($price / $originalPrice) * 100), 2);
}
$currency = 'GBP';
return [
'productName' => $productName,
'productUrl' => $productUrl,
'price' => $price,
'originalPrice' => $originalPrice,
'discountPercentage' => $discountPercentage,
'currency' => $currency
];
}
// Handle Form Submission (Add New Filament to Track)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$productName = $_POST['productName'];
$productUrl = $_POST['productUrl'];
$result = scrapeFilamentPrice($productName, $productUrl);
try {
$stmt = $pdo->prepare("
INSERT INTO filamentPrices (productName, productUrl, price, originalPrice, discountPercentage, currency)
VALUES (:productName, :productUrl, :price, :originalPrice, :discountPercentage, :currency)
");
$stmt->execute([
':productName' => $result['productName'],
':productUrl' => $result['productUrl'],
':price' => $result['price'],
':originalPrice' => $result['originalPrice'],
':discountPercentage' => $result['discountPercentage'],
':currency' => $result['currency']
]);
echo json_encode(['status' => 'success', 'message' => 'Filament price recorded successfully!']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'Failed to record price: ' . $e->getMessage()]);
}
}
// Handle Scheduled Scraping for Existing URLs (Cron Job)
if (isset($argv[1]) && $argv[1] === 'cron') {
try {
$stmt = $pdo->query("SELECT * FROM filamentPrices GROUP BY productUrl");
$filaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($filaments as $filament) {
$result = scrapeFilamentPrice($filament['productName'], $filament['productUrl']);
$stmt = $pdo->prepare("
INSERT INTO filamentPrices (productName, productUrl, price, originalPrice, discountPercentage, currency)
VALUES (:productName, :productUrl, :price, :originalPrice, :discountPercentage, :currency)
");
$stmt->execute([
':productName' => $result['productName'],
':productUrl' => $result['productUrl'],
':price' => $result['price'],
':originalPrice' => $result['originalPrice'],
':discountPercentage' => $result['discountPercentage'],
':currency' => $result['currency']
]);
echo "Recorded: {$result['productName']} - £{$result['price']} (Discount: {$result['discountPercentage']}%)\n";
}
} catch (PDOException $e) {
echo "Failed to fetch filament data: " . $e->getMessage();
}
}
?>

View File

@@ -0,0 +1,63 @@
<?php
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/vendor/autoload.php';
require '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/db.php';
require_once '/mnt/www-live/TechOdyssey_Designs_Dashboard/src/envLoader.php';
loadEnv(__DIR__ . '/../../.env');
use Goutte\Client;
// Function to scrape price
function scrapePrice($url) {
$client = new Client(); // Reinitialize client for each request
try {
$crawler = $client->request('GET', $url);
$whole = $crawler->filter('.a-price-whole')->count() ? $crawler->filter('.a-price-whole')->text() : '0';
$fraction = $crawler->filter('.a-price-fraction')->count() ? $crawler->filter('.a-price-fraction')->text() : '00';
$whole = preg_replace('/[^0-9]/', '', $whole);
$fraction = preg_replace('/[^0-9]/', '', $fraction);
$fraction = strlen($fraction) === 1 ? $fraction . '0' : substr($fraction, 0, 2);
$totalPrice = floatval($whole . '.' . $fraction);
return $totalPrice;
} catch (Exception $e) {
return null;
}
}
// Fetch all filaments
try {
$stmt = $pdo->query("SELECT * FROM filamentTracker");
$filaments = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($filaments as $filament) {
$amazonUrl = $filament['amazonUrl'];
$filamentId = $filament['id'];
// Scrape price
$totalPrice = scrapePrice($amazonUrl);
if ($totalPrice !== null && $totalPrice > 0) {
$stmt = $pdo->prepare("
INSERT INTO filamentPriceHistory (filamentId, price)
VALUES (:filamentId, :price)
");
$stmt->execute([
':filamentId' => $filamentId,
':price' => $totalPrice
]);
echo "Updated price for {$filament['filamentName']}: £{$totalPrice}\n";
} else {
echo "Failed to update {$filament['filamentName']} (no price found or £0).\n";
}
// Add a small random delay between 1 to 5 seconds
sleep(rand(1, 5));
}
} catch (PDOException $e) {
echo "Database error: " . $e->getMessage();
}
?>