diff --git a/app/Config/Routes.php b/app/Config/Routes.php index f435cc7671a8c387be6a55af88fc39fd772323ca..d578e387fb60a65cd2f9ede6c6dcfb4174a54522 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -158,6 +158,14 @@ $routes->group( 'filter' => 'permission:podcasts-view,podcast-view', ] ); + $routes->get( + 'time-periods', + 'Podcast::viewAnalyticsTimePeriods/$1', + [ + 'as' => 'podcast-analytics-time-periods', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); $routes->get( 'players', 'Podcast::viewAnalyticsPlayers/$1', diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index a6e9e2023b7eca6a23dabbcb29ab43cd6705f91e..1e51120e3672ed781679a2110dfc3a74088714b3 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -98,6 +98,14 @@ class Podcast extends BaseController return view('admin/podcast/analytics/listening_time', $data); } + public function viewAnalyticsTimePeriods() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/time_periods', $data); + } + public function viewAnalyticsPlayers() { $data = ['podcast' => $this->podcast]; diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index be49e1059ead0536ac1b8171e48e970d26a66269..a7b26ff79816a1e8ee5bb8666c9dbec55babc870 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -28,4 +28,5 @@ return [ 'unique-listeners' => 'Unique listeners', 'players' => 'Players', 'listening-time' => 'Listening time', + 'time-periods' => 'Time periods', ]; diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php index 206df55635a9a7c244dbecb36281db44185ac473..0b6078d1a1c9dc6833adf591ed47c4184c824efb 100644 --- a/app/Language/en/Charts.php +++ b/app/Language/en/Charts.php @@ -30,4 +30,7 @@ return [ 'podcast_bots' => 'Bots (crawlers)', 'daily_listening_time' => 'Daily cumulative listening time', 'monthly_listening_time' => 'Monthly cumulative listening time', + 'by_weekday' => 'By week day (for the past 60 days)', + 'by_hour' => 'By time of day (for the past 60 days)', + 'podcast_by_bandwidth' => 'Daily used bandwidth (in MB)', ]; diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php index 262884081b421c20e7fb1de45b43b7ef1c6d7004..712fc8bd42e93631316e4fbb12258f24a18b5970 100644 --- a/app/Language/en/PodcastNavigation.php +++ b/app/Language/en/PodcastNavigation.php @@ -26,4 +26,5 @@ return [ 'podcast-analytics-unique-listeners' => 'Unique listeners', 'podcast-analytics-players' => 'Players', 'podcast-analytics-listening-time' => 'Listening time', + 'podcast-analytics-time-periods' => 'Time periods', ]; diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php index 50405e5375dcdeafcfb3eb570e623f06ec0048db..5c55f168f8c1b33df3e97d329a146310e15a77aa 100644 --- a/app/Language/fr/Breadcrumb.php +++ b/app/Language/fr/Breadcrumb.php @@ -28,4 +28,5 @@ return [ 'unique-listeners' => 'Auditeurs uniques', 'players' => 'Lecteurs', 'listening-time' => 'Durée d’écoute', + 'time-periods' => 'Périodes', ]; diff --git a/app/Language/fr/Charts.php b/app/Language/fr/Charts.php index 14607d2690f70ad66014c2a5231396b8ab59a659..1b72c51b440e47bf6f36329fe1c084589c78533b 100644 --- a/app/Language/fr/Charts.php +++ b/app/Language/fr/Charts.php @@ -43,4 +43,7 @@ return [ 'podcast_bots' => 'Robots (bots)', 'daily_listening_time' => 'Durée quotidienne d’écoute cumulée', 'monthly_listening_time' => 'Durée mensuelle d’écoute cumulée', + 'by_weekday' => 'Par jour de la semaine (sur les 60 derniers jours)', + 'by_hour' => 'Par heure de la journée (sur les 60 derniers jours)', + 'podcast_by_bandwidth' => 'Bande passante quotidienne consommée (en Mo)', ]; diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php index 9a2e09e93f5918e1281644ca1bc0258a77b6b427..398521866c976dac4e730e17c010d00ad40aa690 100644 --- a/app/Language/fr/PodcastNavigation.php +++ b/app/Language/fr/PodcastNavigation.php @@ -26,4 +26,5 @@ return [ 'podcast-analytics-unique-listeners' => 'Auditeurs uniques', 'podcast-analytics-players' => 'Lecteurs', 'podcast-analytics-listening-time' => 'Durée d’écoute', + 'podcast-analytics-time-periods' => 'Périodes', ]; diff --git a/app/Models/AnalyticsPodcastByCountryModel.php b/app/Models/AnalyticsPodcastByCountryModel.php index 162170a5cf7c0c47c904e8b969537d0b9844c4eb..ad9bb39585e5a3e0a1bcf1a0d89f04127ad386f8 100644 --- a/app/Models/AnalyticsPodcastByCountryModel.php +++ b/app/Models/AnalyticsPodcastByCountryModel.php @@ -37,15 +37,33 @@ class AnalyticsPodcastByCountryModel extends Model "{$podcastId}_analytics_podcast_by_country_weekly" )) ) { + $oneWeekAgo=date('Y-m-d', strtotime('-1 week')); $found = $this->select('`country_code` as `labels`') ->selectSum('`hits`', '`values`') ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 week')), + '`date` >' => $oneWeekAgo, ]) ->groupBy('`labels`') ->orderBy('`values`', 'DESC') - ->findAll(10); + ->findAll(9); + + $found[] = $this->db + ->query( + "SELECT + \"Other\" AS `labels`, + SUM(`others`.`values`) AS `values` + FROM + (SELECT SUM(`hits`) AS `values` + FROM {$this->db->prefixTable($this->table)} + WHERE `date` > $oneWeekAgo + GROUP BY `country_code` + ORDER BY `values` DESC + LIMIT 18446744073709551610 OFFSET 9 + ) AS `others` + GROUP BY `labels`" + ) + ->getRow(0); cache()->save( "{$podcastId}_analytics_podcast_by_country_weekly", @@ -70,16 +88,34 @@ class AnalyticsPodcastByCountryModel extends Model "{$podcastId}_analytics_podcast_by_country_yearly" )) ) { + $oneYearAgo = date('Y-m-d', strtotime('-1 year')); $found = $this->select('`country_code` as `labels`') ->selectSum('`hits`', '`values`') ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 year')), + '`date` >' => $oneYearAgo, ]) ->groupBy('`labels`') ->orderBy('`values`', 'DESC') ->findAll(10); + $found[] = $this->db + ->query( + "SELECT + \"Other\" AS `labels`, + SUM(`others`.`values`) AS `values` + FROM + (SELECT SUM(`hits`) AS `values` + FROM {$this->db->prefixTable($this->table)} + WHERE `date` > $oneyearago + GROUP BY `country_code` + ORDER BY `values` DESC + LIMIT 18446744073709551610 OFFSET 9 + ) AS `others` + GROUP BY `labels`" + ) + ->getRow(0); + cache()->save( "{$podcastId}_analytics_podcast_by_country_yearly", $found, diff --git a/app/Models/AnalyticsPodcastByHourModel.php b/app/Models/AnalyticsPodcastByHourModel.php index 9dd82a4d1a12cc1a6a832f45f705245224979689..1960e7836e66e088941a1a701831ad7222f2cf5f 100644 --- a/app/Models/AnalyticsPodcastByHourModel.php +++ b/app/Models/AnalyticsPodcastByHourModel.php @@ -22,4 +22,38 @@ class AnalyticsPodcastByHourModel extends Model protected $useSoftDeletes = false; protected $useTimestamps = false; + + /** + * Gets hits data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getData(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcasts_by_hour"))) { + $found = $this->select('`hour` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-60 days')), + ]) + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_hour", + $found, + 600 + ); + } + return $found; + } + + + + + } diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Models/AnalyticsPodcastModel.php index e2c60ff583c0271cc841991c956df24e6bb71c6b..1d44a55150486d739abe691c8cfb02bedd3d90fe 100644 --- a/app/Models/AnalyticsPodcastModel.php +++ b/app/Models/AnalyticsPodcastModel.php @@ -46,6 +46,66 @@ class AnalyticsPodcastModel extends Model return $found; } + /** + * Gets hits data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByWeekday(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcasts_by_weekday"))) { + $found = $this->select( + 'LEFT(DAYNAME(`date`),3) as `labels`, WEEKDAY(`date`) as `sort_labels`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-60 days')), + ]) + ->groupBy('`labels`, `sort_labels`') + ->orderBy('`sort_labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_weekday", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets bandwidth data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataBandwidthByDay(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcast_by_bandwidth"))) { + $found = $this->select( + '`date` as `labels`, round(`bandwidth` / 1048576, 1) as `values`' + ) + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-60 days')), + ]) + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_by_bandwidth", + $found, + 600 + ); + } + return $found; + } + /** * Gets hits data for a podcast * diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index 41149edd4b74936c337b4857a6d2e9c79fc854d9..9e2f754e945e49d31d83bf917ae7ccea9c62a12a 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -52,7 +52,7 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { const series = chart.series.push(new am4charts.LineSeries()); series.dataFields.valueY = "values"; series.dataFields.dateX = "labels"; - series.tooltipText = "{valueY} hits"; + series.tooltipText = "{valueY}"; series.strokeWidth = 2; // Make bullets grow on hover const bullet = series.bullets.push(new am4charts.CircleBullet()); @@ -68,6 +68,35 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { chart.scrollbarX = new am4core.Scrollbar(); }; +const drawBarChart = (chartDivId: string, dataUrl: string | null): void => { + // Create chart instance + const chart = am4core.create(chartDivId, am4charts.XYChart); + am4core.percent(100); + chart.exporting.menu = new am4core.ExportMenu(); + chart.exporting.menu.align = "right"; + chart.exporting.menu.verticalAlign = "bottom"; + // Set theme + am4core.useTheme(am4themes_material); + chart.dataSource.url = dataUrl || ""; + chart.dataSource.parser.options.emptyAs = 0; + const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis()); + categoryAxis.dataFields.category = "labels"; + categoryAxis.renderer.grid.template.location = 0; + categoryAxis.renderer.minGridDistance = 30; + chart.yAxes.push(new am4charts.ValueAxis()); + // Create series + const series = chart.series.push(new am4charts.ColumnSeries()); + series.dataFields.valueY = "values"; + series.dataFields.categoryX = "labels"; + series.name = "Hits"; + series.columns.template.tooltipText = "{valueY} hits"; + series.columns.template.fillOpacity = .8; + const columnTemplate = series.columns.template; + columnTemplate.strokeWidth = 2; + columnTemplate.strokeOpacity = 1; +}; + + const drawXYDurationChart = ( chartDivId: string, dataUrl: string | null @@ -205,6 +234,9 @@ const DrawCharts = (): void => { case "xy-chart": drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); break; + case "bar-chart": + drawBarChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; case "xy-duration-chart": drawXYDurationChart( chartDiv.id, diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index cf637465041739271b12909d856516ae721a2e5f..704a744405b8ce269d88cad03ccba200ae494738 100644 --- a/app/Views/admin/podcast/_sidebar.php +++ b/app/Views/admin/podcast/_sidebar.php @@ -16,6 +16,7 @@ $podcastNavigation = [ 'podcast-analytics-listening-time', 'podcast-analytics-players', 'podcast-analytics-locations', + 'podcast-analytics-time-periods', 'podcast-analytics-webpages', ], ], diff --git a/app/Views/admin/podcast/analytics/index.php b/app/Views/admin/podcast/analytics/index.php index f604d1753659e86e4e56f2ba42bcb98c16e6ddc4..54b9544389bea3aca73002e5d21391e9ad949b36 100644 --- a/app/Views/admin/podcast/analytics/index.php +++ b/app/Views/admin/podcast/analytics/index.php @@ -29,6 +29,16 @@ ) ?>"></div> </div> +<div class="mb-12 text-center"> +<h2><?= lang('Charts.podcast_by_bandwidth') ?></h2> +<div class="chart-xy" id="by-bandwidth-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'BandwidthByDay' +) ?>"></div> +</div> + <div class="mb-12 text-center"> <h2><?= lang('Charts.episodes_by_day') ?></h2> <div class="chart-xy" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to( diff --git a/app/Views/admin/podcast/analytics/time_periods.php b/app/Views/admin/podcast/analytics/time_periods.php new file mode 100644 index 0000000000000000000000000000000000000000..8b568730722eafe59764a4d42dcb40041bd6a1f4 --- /dev/null +++ b/app/Views/admin/podcast/analytics/time_periods.php @@ -0,0 +1,36 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="lg:divide-x lg:grid lg:grid-cols-2"> + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_weekday') ?></h2> + <div class="chart-xy" id="by-weekday-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'ByWeekday' + ) ?>"></div> + </div> + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_hour') ?></h2> + <div class="chart-xy" id="by-hour-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'PodcastByHour' + ) ?>"></div> + </div> + +</div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?>