diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 878e0cc80a99ccd56346a7eea903f3016017ab70..9e1209c07c325c017bc88e4c2b92f9f821b23220 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -153,6 +153,14 @@ $routes->group( 'filter' => 'permission:podcasts-view,podcast-view', ] ); + $routes->get( + 'listening-time', + 'Podcast::viewAnalyticsListeningTime/$1', + [ + 'as' => 'podcast-analytics-listening-time', + '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 2cf43a4736140e26604999dfc36a63836fe9e3e1..1d10f9f2f0b6a92c25100a979609727f3d927063 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -90,6 +90,14 @@ class Podcast extends BaseController return view('admin/podcast/analytics/unique_listeners', $data); } + public function viewAnalyticsListeningTime() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/listening-time', $data); + } + public function viewAnalyticsPlayers() { $data = ['podcast' => $this->podcast]; diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index dba5b87679ca143b365ef6c044f8508312e268f7..be49e1059ead0536ac1b8171e48e970d26a66269 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -27,4 +27,5 @@ return [ 'webpages' => 'Web pages', 'unique-listeners' => 'Unique listeners', 'players' => 'Players', + 'listening-time' => 'Listening time', ]; diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php index 5e3e8879d6e8707970fdf8c23b9db3e1c5089fa4..ac27218ebea87e31da682f9e80b78d5e0a664c5a 100644 --- a/app/Language/en/Charts.php +++ b/app/Language/en/Charts.php @@ -27,4 +27,6 @@ return [ 'by_domain_yearly' => 'Web pages visits by source (for the past year)', 'by_entry_page' => 'Web pages visits by landing page (for the past week)', 'podcast_bots' => 'Bots (crawlers)', + 'daily_listening_time' => 'Daily cumulative listening time', + 'monthly_listening_time' => 'Monthly cumulative listening time', ]; diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php index 9dae71b3c8349030facf852be33dc7509df839a1..262884081b421c20e7fb1de45b43b7ef1c6d7004 100644 --- a/app/Language/en/PodcastNavigation.php +++ b/app/Language/en/PodcastNavigation.php @@ -25,4 +25,5 @@ return [ 'podcast-analytics-locations' => 'Locations', 'podcast-analytics-unique-listeners' => 'Unique listeners', 'podcast-analytics-players' => 'Players', + 'podcast-analytics-listening-time' => 'Listening time', ]; diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php index a27e5278fa0bd19bb2c3044079c9646b807191a8..50405e5375dcdeafcfb3eb570e623f06ec0048db 100644 --- a/app/Language/fr/Breadcrumb.php +++ b/app/Language/fr/Breadcrumb.php @@ -27,4 +27,5 @@ return [ 'webpages' => 'Pages web', 'unique-listeners' => 'Auditeurs uniques', 'players' => 'Lecteurs', + 'listening-time' => 'Durée d’écoute', ]; diff --git a/app/Language/fr/Charts.php b/app/Language/fr/Charts.php index 2469f6fbb2b92f318c2715519cd9d232a4dc6189..b878234d1056c1e1f26da65d6cf3543cee8de2c7 100644 --- a/app/Language/fr/Charts.php +++ b/app/Language/fr/Charts.php @@ -39,4 +39,6 @@ return [ 'by_entry_page' => 'Fréquentation des pages web par page d’entrée (sur la dernière semaine)', 'podcast_bots' => 'Robots (bots)', + 'daily_listening_time' => 'Durée quotidienne d’écoute cumulée', + 'monthly_listening_time' => 'Durée mensuelle d’écoute cumulée', ]; diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php index ea4838374670172f89dbb4d071b474dc28e751d7..9a2e09e93f5918e1281644ca1bc0258a77b6b427 100644 --- a/app/Language/fr/PodcastNavigation.php +++ b/app/Language/fr/PodcastNavigation.php @@ -25,4 +25,5 @@ return [ 'podcast-analytics-locations' => 'Localisations', 'podcast-analytics-unique-listeners' => 'Auditeurs uniques', 'podcast-analytics-players' => 'Lecteurs', + 'podcast-analytics-listening-time' => 'Durée d’écoute', ]; diff --git a/app/Models/AnalyticsPodcastByEpisodeModel.php b/app/Models/AnalyticsPodcastByEpisodeModel.php index fce1544546bf690aeecc7bec00ec1788b7355712..29506bda7eadcd821162fa65313ce31858f0177a 100644 --- a/app/Models/AnalyticsPodcastByEpisodeModel.php +++ b/app/Models/AnalyticsPodcastByEpisodeModel.php @@ -142,4 +142,71 @@ class AnalyticsPodcastByEpisodeModel extends Model } return $found; } + + /** + * Gets listening-time data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataTotalListeningTimeByDay(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_listening_time_by_day" + )) + ) { + $found = $this->select('date as labels') + ->selectSum('(enclosure_duration * hits)', 'values') + ->join('episodes', 'id = episode_id', 'inner') + ->where([ + $this->table . '.podcast_id' => $podcastId, + 'date >' => date('Y-m-d', strtotime('-60 days')), + ]) + ->groupBy('labels') + ->orderBy('labels', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_listening_time_by_day", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets listening-time data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataTotalListeningTimeByMonth(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_listening_time_by_month" + )) + ) { + $found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`') + ->selectSum('(enclosure_duration * hits)', 'values') + ->join('episodes', 'id = episode_id', 'inner') + ->where([ + $this->table . '.podcast_id' => $podcastId, + ]) + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_listening_time_by_month", + $found, + 600 + ); + } + return $found; + } } diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Models/AnalyticsPodcastByRegionModel.php index 45edbc2413d37e4b612a58b0e98e60d2fb60b51b..511a0769b17157296b3eb167abbbd5895b594180 100644 --- a/app/Models/AnalyticsPodcastByRegionModel.php +++ b/app/Models/AnalyticsPodcastByRegionModel.php @@ -38,13 +38,11 @@ class AnalyticsPodcastByRegionModel extends Model "{$podcastId}_analytics_podcast_by_region_{$locale}" )) ) { - $found = $this->select( - '`country_code`, `region_code`, `latitude`, `longitude`' - ) + $found = $this->select('`country_code`, `region_code`') ->selectSum('`hits`', '`value`') - ->groupBy( - '`country_code`, `region_code`, `latitude`, `longitude`' - ) + ->selectAvg('`latitude`') + ->selectAvg('`longitude`') + ->groupBy('`country_code`, `region_code`') ->where([ '`podcast_id`' => $podcastId, '`date` >' => date('Y-m-d', strtotime('-1 week')), diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index b8d112077c2e683be49f4fdf443404c469c9ab1b..3266714ec47124637d0c39fbb3bb0c9e5ae1d04f 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -62,6 +62,41 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { chart.scrollbarX = new am4core.Scrollbar(); }; +const drawXYDurationChart = (chartDivId: string, dataUrl: string | null): void => { + // Create chart instance + const chart = am4core.create(chartDivId, am4charts.XYChart); + am4core.percent(100); + // Set theme + am4core.useTheme(am4themes_material); + // Create axes + const dateAxis = chart.xAxes.push(new am4charts.DateAxis()); + dateAxis.renderer.minGridDistance = 60; + const yAxis = chart.yAxes.push(new am4charts.DurationAxis()); + yAxis.baseUnit = "second"; + chart.durationFormatter.durationFormat = "hh'h,' mm'mn'"; + // Add data + chart.dataSource.url = dataUrl || ""; + chart.dataSource.parser.options.emptyAs = 0; + // Create series + const series = chart.series.push(new am4charts.LineSeries()); + series.dataFields.valueY = "values"; + series.dataFields.dateX = "labels"; + series.tooltipText = "{valueY.formatDuration()}"; + series.strokeWidth = 2; + // Make bullets grow on hover + const bullet = series.bullets.push(new am4charts.CircleBullet()); + bullet.circle.strokeWidth = 2; + bullet.circle.radius = 4; + bullet.circle.fill = am4core.color("#fff"); + const bullethover = bullet.states.create("hover"); + bullethover.properties.scale = 1.3; + series.tooltip.pointerOrientation = "vertical"; + chart.cursor = new am4charts.XYCursor(); + chart.cursor.snapToSeries = series; + chart.cursor.xAxis = dateAxis; + chart.scrollbarX = new am4core.Scrollbar(); +}; + const drawXYSeriesChart = ( chartDivId: string, dataUrl: string | null @@ -152,6 +187,9 @@ const DrawCharts = (): void => { case "xy-chart": drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); break; + case "xy-duration-chart": + drawXYDurationChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; case "xy-series-chart": drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); break; diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index d62dd8573fc08e009422a74225371bc61abfb0ba..cf637465041739271b12909d856516ae721a2e5f 100644 --- a/app/Views/admin/podcast/_sidebar.php +++ b/app/Views/admin/podcast/_sidebar.php @@ -13,6 +13,7 @@ $podcastNavigation = [ 'items' => [ 'podcast-analytics', 'podcast-analytics-unique-listeners', + 'podcast-analytics-listening-time', 'podcast-analytics-players', 'podcast-analytics-locations', 'podcast-analytics-webpages', diff --git a/app/Views/admin/podcast/analytics/listening-time.php b/app/Views/admin/podcast/analytics/listening-time.php new file mode 100644 index 0000000000000000000000000000000000000000..f6cd05d0a26e828235db4f50f6ede82c6836ce4e --- /dev/null +++ b/app/Views/admin/podcast/analytics/listening-time.php @@ -0,0 +1,34 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.daily_listening_time') ?></h2> +<div class="chart-xy" id="by-day-listening-time-graph" data-chart-type="xy-duration-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByEpisode', + 'TotalListeningTimeByDay' +) ?>"></div> +</div> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.monthly_listening_time') ?></h2> +<div class="chart-xy" id="by-month-listening-time-graph" data-chart-type="xy-duration-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByEpisode', + 'TotalListeningTimeByMonth' +) ?>"></div> +</div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?>