From 588b4d28da00bc12d02126e23181690f54d81716 Mon Sep 17 00:00:00 2001
From: Benjamin Bellamy <ben@podlibre.org>
Date: Mon, 19 Oct 2020 10:33:23 +0000
Subject: [PATCH] feat: add cumulative listening time charts

---
 app/Config/Routes.php                         |  8 +++
 app/Controllers/Admin/Podcast.php             |  8 +++
 app/Language/en/Breadcrumb.php                |  1 +
 app/Language/en/Charts.php                    |  2 +
 app/Language/en/PodcastNavigation.php         |  1 +
 app/Language/fr/Breadcrumb.php                |  1 +
 app/Language/fr/Charts.php                    |  2 +
 app/Language/fr/PodcastNavigation.php         |  1 +
 app/Models/AnalyticsPodcastByEpisodeModel.php | 67 +++++++++++++++++++
 app/Models/AnalyticsPodcastByRegionModel.php  | 10 ++-
 app/Views/_assets/modules/Charts.ts           | 38 +++++++++++
 app/Views/admin/podcast/_sidebar.php          |  1 +
 .../podcast/analytics/listening-time.php      | 34 ++++++++++
 13 files changed, 168 insertions(+), 6 deletions(-)
 create mode 100644 app/Views/admin/podcast/analytics/listening-time.php

diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 878e0cc80a..9e1209c07c 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 2cf43a4736..1d10f9f2f0 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 dba5b87679..be49e1059e 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 5e3e8879d6..ac27218ebe 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 9dae71b3c8..262884081b 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 a27e5278fa..50405e5375 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 2469f6fbb2..b878234d10 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 ea48383746..9a2e09e93f 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 fce1544546..29506bda7e 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 45edbc2413..511a0769b1 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 b8d112077c..3266714ec4 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 d62dd8573f..cf63746504 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 0000000000..f6cd05d0a2
--- /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() ?>
-- 
GitLab