Add monthly views chart

This commit is contained in:
2025-10-08 16:52:40 +02:00
parent 2880547f3e
commit fffa320c08
8 changed files with 144 additions and 30 deletions

View File

@@ -60,13 +60,6 @@ class CacheHelper
});
}
public static function getTotalMonthlyViews()
{
return Cache::remember("total_monthly_view_count", now()->addMinutes(60), function () {
return PopularMonthly::count();
});
}
public static function getPopularAllTime(bool $guest)
{
$guestString = $guest ? 'guest' : 'authed';

View File

@@ -2,16 +2,18 @@
namespace App\Http\Controllers\Api;
use App\Models\Downloads;
use App\Models\Hentai;
use App\Models\PopularMonthly;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Http\Controllers\Controller;
class HentaiApiController extends Controller
{
public function index(Request $request)
/**
* Get a list of all hentai with it's episodes
*/
public function index()
{
// Cache for 10 minutes
$data = Cache::remember('api_hentai_list', now()->addMinutes(10), function () {
@@ -35,4 +37,20 @@ class HentaiApiController extends Controller
return response()->json($data);
}
/**
* Get monthly views by day for stats
*/
public function getMonthlyViews()
{
// Cache for 60 minutes
$data = Cache::remember('api_hentai_list', now()->addMinutes(60), function () {
return PopularMonthly::selectRaw('DATE(created_at) as date, COUNT(*) as count')
->groupBy('date')
->orderBy('date', 'asc')
->get();
});
return response()->json($data);
}
}

View File

@@ -87,7 +87,6 @@ class HomeController extends Controller
'viewCount' => CacheHelper::getTotalViewCount(),
'episodeCount' => CacheHelper::getTotalEpisodeCount(),
'hentaiCount' => CacheHelper::getTotalHentaiCount(),
'monthlyCount' => CacheHelper::getTotalMonthlyViews()
]);
}

50
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",
"plyr": "^3.7.8",
@@ -572,6 +573,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1267,10 +1274,16 @@
"license": "CC-BY-4.0"
},
"node_modules/chart.js": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==",
"license": "MIT"
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
"integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/chartjs-plugin-datalabels": {
"version": "2.2.0",
@@ -3142,6 +3155,12 @@
"tailwindcss": "3.3.0"
}
},
"node_modules/tw-elements/node_modules/chart.js": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz",
"integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==",
"license": "MIT"
},
"node_modules/tw-elements/node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -3258,6 +3277,15 @@
"postcss": "^8.0.9"
}
},
"node_modules/tw-elements/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
@@ -3615,12 +3643,18 @@
}
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 6"
"node": ">= 14.6"
}
}
}

View File

@@ -19,6 +19,7 @@
"@fortawesome/fontawesome-free": "^6.5.1",
"@jellyfin/libass-wasm": "^4.1.1",
"@yaireo/tagify": "^4.21.2",
"chart.js": "^4.5.0",
"dashjs": "^5.0.0",
"hammerjs": "^2.0.8",
"plyr": "^3.7.8",

73
resources/js/stats.js Normal file
View File

@@ -0,0 +1,73 @@
import Chart from 'chart.js/auto';
// Theming
if (localStorage.theme !== 'light') {
Chart.defaults.color = "#ADBABD";
Chart.defaults.borderColor = "rgba(255,255,255,0.1)";
Chart.defaults.backgroundColor = "rgba(255,255,0,0.1)";
Chart.defaults.elements.line.borderColor = "rgba(255,255,0,0.4)";
}
// Get Tags from API
window.axios.get('/v1/monthly-views').then(function (response) {
if (response.status != 200) {
return;
}
const data = {
labels: response.data.map((entry) => { return entry.date }),
datasets: [{
label: 'Views',
fill: false,
backgroundColor: 'rgba(190, 18, 60, 0.3)',
borderColor: 'rgba(190, 18, 60, 1.0)',
cubicInterpolationMode: 'monotone',
data: response.data.map((entry) => { return entry.count }),
}]
}
const config = {
type: 'line',
data: data,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Views the last 30 days',
font: {
size: 18
}
},
},
interaction: {
intersect: false,
},
scales: {
x: {
display: true,
title: {
display: true
}
},
y: {
display: true,
title: {
display: true,
text: 'Views'
},
suggestedMin: 0,
suggestedMax: 40000
}
}
},
};
const monthlyViewChart = new Chart(
document.getElementById('monthlyChart'),
config
);
}).catch(function (error) {
console.log(error);
});

View File

@@ -4,7 +4,7 @@
<div class="flex justify-center pb-10">
<img src="/images/cropped-HS-1-270x270.webp" class="max-w-[150px]" alt="hstream.moe Logo" />
</div>
<div class="grid md:grid-cols-5 lg:gap-x-12">
<div class="grid md:grid-cols-2 lg:grid-cols-4 lg:gap-x-12">
<div class="mb-12 md:mb-0">
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-sky-500">
<i class="fa-solid fa-eye text-3xl"> {{ number_format($viewCount) }}</i>
@@ -14,15 +14,7 @@
</h5>
</div>
<div class="mb-12 md:mb-0">
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-sky-500">
<i class="fa-solid fa-calendar-days text-3xl"> {{ number_format($monthlyCount) }}</i>
</div>
<h5 class="text-lg font-medium dark:text-neutral-300">
views the last 30 days
</h5>
</div>
<div class="mb-12 md:mb-0">
<div class="mb-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-rose-600">
<div class="b-6 inline-block rounded-md bg-white dark:bg-neutral-950 p-4 text-sky-500">
<i class="fa-solid fa-video text-3xl"> {{ $episodeCount }}</i>
</div>
<h5 class="text-lg font-medium dark:text-neutral-300">
@@ -46,7 +38,10 @@
</h5>
</div>
</div>
</section>
<p class="text-center text-black/40 dark:text-white/40">Cached for 60 Minutes</p>
<div class="flex justify-center dark:bg-neutral-950 bg-gray-50 rounded-xl md:m-11 hidden md:block">
<canvas id="monthlyChart"></canvas>
</div>
</section>
</div>
@vite(['resources/js/stats.js'])
</x-app-layout>

View File

@@ -26,6 +26,7 @@ Route::get('/banned', [HomeController::class, 'banned'])->name('home.banned');
// API Endpoint
Route::get('/v1/hentai-list', [HentaiApiController::class, 'index'])->name('api.hentai.index');
Route::get('/v1/monthly-views', [HentaiApiController::class, 'getMonthlyViews'])->name('api.hentai.monthly');
// Stream Page
Route::get('/hentai/{title}', [StreamController::class, 'index'])->name('hentai.index');