diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 5d2c45f8051cdec4245b05133294d46209919b99..159afc4764cdd67b87fae28ec034ced162b1e3fd 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -121,6 +121,14 @@ $routes->group(
                     'as' => 'podcast-analytics',
                     'filter' => 'permission:podcasts-view,podcast-view',
                 ]);
+                $routes->get(
+                    'analytics-data/(:segment)',
+                    'AnalyticsData::getData/$1/$2',
+                    [
+                        'as' => 'analytics-full-data',
+                        'filter' => 'permission:podcasts-view,podcast-view',
+                    ]
+                );
                 $routes->get(
                     'analytics-data/(:segment)/(:segment)',
                     'AnalyticsData::getData/$1/$2/$3',
diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php
index a57960dfd1561982dbdf33aab4d32bd2ba8657f3..ba5e1673a39021db6f2430d792813daf8cec20e0 100644
--- a/app/Controllers/Admin/AnalyticsData.php
+++ b/app/Controllers/Admin/AnalyticsData.php
@@ -23,14 +23,15 @@ class AnalyticsData extends BaseController
 
     public function _remap($method, ...$params)
     {
-        if (count($params) > 2) {
+        if (count($params) > 1) {
             if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
                 throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
                     'Podcast not found: ' . $params[0]
                 );
             }
             $this->className = '\App\Models\Analytics' . $params[1] . 'Model';
-            $this->methodName = 'getData' . $params[2];
+            $this->methodName =
+                'getData' . (empty($params[2]) ? '' : $params[2]);
             if (count($params) > 3) {
                 if (
                     !($this->episode = (new EpisodeModel())
diff --git a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
index e31d932d5f758bdd897334ffba807fea667b10d2..041a195a98109a3230ce4d04b77b8d910fb0eb7e 100644
--- a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
+++ b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AddAnalyticsPodcastsByCountry
- * Creates analytics_podcasts_by_country table in database
+ * Class AddAnalyticsPodcasts
+ * Creates analytics_podcasts table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -30,6 +30,11 @@ class AddAnalyticsPodcasts extends Migration
                 'constraint' => 10,
                 'default' => 1,
             ],
+            'unique_listeners' => [
+                'type' => 'INT',
+                'constraint' => 10,
+                'default' => 1,
+            ],
         ]);
         $this->forge->addPrimaryKey(['podcast_id', 'date']);
         $this->forge->addField(
diff --git a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
index f26977ac01f1ab9ab8f4dad13986069b79257b87..c9c50221ea13651da5909077450a581bb23e4b86 100644
--- a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
+++ b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AddAnalyticsEpisodesByCountry
- * Creates analytics_episodes_by_country table in database
+ * Class AddAnalyticsPodcastsByEpisode
+ * Creates analytics_episodes_by_episode table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
diff --git a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
index 426783c290b752e696638867fe9679cb32822c18..78355e86798bfaa72d2feda2d20685b3f2243934 100644
--- a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
+++ b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AddAnalyticsWebsiteByReferer
- * Creates analytics_website_by_referer table in database
+ * Class AddAnalyticsWebsiteByEntryPage
+ * Creates analytics_website_by_entry_page table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
index caf355009a0420b2581c9cab9439c9737bb29642..7089e51e627a18df346d6bd317848261ec9096cd 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
+++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
@@ -17,7 +17,7 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration
     public function up()
     {
         // Creates Stored Procedure for data insertion
-        // Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer');
+        // Example: CALL analytics_podcasts(1, 2, 'FR', 'IDF', 48.853, 2.349, PodcastAddict, 'phone', 'android', 0, 1);
         $prefix = $this->db->getPrefix();
 
         $createQuery = <<<EOD
@@ -31,7 +31,8 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` (
     IN `p_app` VARCHAR(128) CHARSET utf8mb4,
     IN `p_device` VARCHAR(32) CHARSET utf8mb4,
     IN `p_os` VARCHAR(32) CHARSET utf8mb4,
-    IN `p_bot` TINYINT(1) UNSIGNED
+    IN `p_bot` TINYINT(1) UNSIGNED,
+    IN `p_new_listener` TINYINT(1) UNSIGNED
     )  MODIFIES SQL DATA
 DETERMINISTIC
 SQL SECURITY INVOKER
@@ -40,7 +41,7 @@ BEGIN
 IF NOT `p_bot` THEN
     INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`) 
         VALUES (p_podcast_id, DATE(NOW())) 
-        ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+        ON DUPLICATE KEY UPDATE `hits`=`hits`+1, `unique_listeners`=`unique_listeners`+`p_new_listener`;
     INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) 
     SELECT p_podcast_id, p_episode_id, DATE(NOW()), datediff(now(),`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id
         ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
index e312a3a0160e6c8838afcc2e529d00d99c5984a4..e17e177ddc6f93cb48f811e3511838887293b219 100644
--- a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
+++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
@@ -114,6 +114,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
                             'podcast_id' => $podcast->id,
                             'date' => date('Y-m-d', $date),
                             'hits' => $hits,
+                            'unique_listeners' => $hits,
                         ];
                         $analytics_podcasts_by_country[] = [
                             'podcast_id' => $podcast->id,
diff --git a/app/Entities/AnalyticsPodcasts.php b/app/Entities/AnalyticsPodcasts.php
index 7f0f169e17cc6a33da15a39890de8b373ccf764e..b15e94e367cf2455b92a2159eb89ef83780e1efd 100644
--- a/app/Entities/AnalyticsPodcasts.php
+++ b/app/Entities/AnalyticsPodcasts.php
@@ -18,5 +18,6 @@ class AnalyticsPodcasts extends Entity
         'podcast_id' => 'integer',
         'date' => 'datetime',
         'hits' => 'integer',
+        'unique_listeners' => 'integer',
     ];
 }
diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php
index cef4e0ac2de225229e7cb472d0009053e613596c..5c242d27c1fa6ae1c2cbf2193a8a9751bd8f182c 100644
--- a/app/Helpers/analytics_helper.php
+++ b/app/Helpers/analytics_helper.php
@@ -199,7 +199,7 @@ function webpage_hit($podcast_id)
 
         $referer = $session->get('referer');
         $domain = empty(parse_url($referer, PHP_URL_HOST))
-            ? null
+            ? '- Direct -'
             : parse_url($referer, PHP_URL_HOST);
         parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
         $keywords = empty($queries['q']) ? null : $queries['q'];
@@ -248,9 +248,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
         if ($session->get('denyListIp')) {
             $session->get('player')['bot'] = true;
         }
-        $httpRange = $_SERVER['HTTP_RANGE'];
-        // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID:
-        $hashID =
+        //We get the HTTP header field `Range`:
+        $httpRange = isset($_SERVER['HTTP_RANGE'])
+            ? $_SERVER['HTTP_RANGE']
+            : null;
+
+        // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID (used to count only once multiple episode downloads):
+        $episodeHashId =
             '_IpUaEp_' .
             sha1(
                 $_SERVER['REMOTE_ADDR'] .
@@ -260,12 +264,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
                     $episodeId
             );
         // Was this episode downloaded in the past 24h:
-        $downloadedBytes = cache($hashID);
+        $downloadedBytes = cache($episodeHashId);
         // Rolling window is 24 hours (86400 seconds):
-        $ttl = 86400;
+        $rollingTTL = 86400;
         if ($downloadedBytes) {
             // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
-            $ttl = cache()->getMetadata($hashID)['expire'] - time();
+            $rollingTTL =
+                cache()->getMetadata($episodeHashId)['expire'] - time();
         } else {
             // If it was never downloaded that means that zero byte were downloaded:
             $downloadedBytes = 0;
@@ -274,7 +279,7 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
         // (Otherwise it means that this was already counted, therefore we don't do anything)
         if ($downloadedBytes < $bytesThreshold) {
             // If HTTP_RANGE is null we are downloading the complete file:
-            if (!isset($httpRange)) {
+            if (!$httpRange) {
                 $downloadedBytes = $fileSize;
             } else {
                 // [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working.
@@ -291,19 +296,44 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
                 }
             }
             // We save the number of downloaded bytes for this user and this episode:
-            cache()->save($hashID, $downloadedBytes, $ttl);
+            cache()->save($episodeHashId, $downloadedBytes, $rollingTTL);
 
-            // If more that 1mn was downloaded, we send that to the database:
+            // If more that 1mn was downloaded, that's a hit, we send that to the database:
             if ($downloadedBytes >= $bytesThreshold) {
                 $db = \Config\Database::connect();
                 $procedureName = $db->prefixTable('analytics_podcasts');
 
+                // We create a sha1 hash for this IP_Address+User_Agent+Podcast_ID (used to count unique listeners):
+                $listenerHashId =
+                    '_IpUaPo_' .
+                    sha1(
+                        $_SERVER['REMOTE_ADDR'] .
+                            '_' .
+                            $_SERVER['HTTP_USER_AGENT'] .
+                            '_' .
+                            $podcastId
+                    );
+                $newListener = 1;
+                // Has this listener already downloaded an episode today:
+                $downloadsByUser = cache($listenerHashId);
+                // We add one download
+                if ($downloadsByUser) {
+                    $newListener = 0;
+                    $downloadsByUser++;
+                } else {
+                    $downloadsByUser = 1;
+                }
+                // Listener count is calculated from 00h00 to 23h59:
+                $midnightTTL = strtotime('tomorrow') - time();
+                // We save the download count for this user until midnight:
+                cache()->save($listenerHashId, $downloadsByUser, $midnightTTL);
+
                 $app = $session->get('player')['app'];
                 $device = $session->get('player')['device'];
                 $os = $session->get('player')['os'];
                 $bot = $session->get('player')['bot'];
 
-                $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [
+                $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?);", [
                     $podcastId,
                     $episodeId,
                     $session->get('location')['countryCode'],
@@ -314,10 +344,12 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
                     $device == null ? '' : $device,
                     $os == null ? '' : $os,
                     $bot == null ? 0 : $bot,
+                    $newListener,
                 ]);
             }
         }
     } catch (\Exception $e) {
         // If things go wrong the show must go on and the user must be able to download the file
+        log_message('critical', $e);
     }
 }
diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php
new file mode 100644
index 0000000000000000000000000000000000000000..c654e32ad921415928c2cf201d50d6925a5f3af0
--- /dev/null
+++ b/app/Language/en/Charts.php
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+return [
+    'by_player' => 'Podcast downloads by player (for the past week)',
+    'unique_daily_listeners' => 'Daily unique listeners',
+    'unique_monthly_listeners' => 'Monthly unique listeners',
+    'by_browser' => 'Website usage by browser (for the past week)',
+    'podcast_by_day' => 'Podcast daily downloads',
+    'podcast_by_month' => 'Podcast monthly downloads',
+    'episodes_by_day' =>
+        '5 latest episodes downloads (during their first 60 days)',
+    'by_country' => 'Podcast downloads by country (for the past week)',
+    'by_domain' => 'Website visits by origin (for the past week)',
+];
diff --git a/app/Models/AnalyticsEpisodesByCountryModel.php b/app/Models/AnalyticsEpisodesByCountryModel.php
deleted file mode 100644
index 25644114abd29c7f0d58f6c89b41b7248f0e2aac..0000000000000000000000000000000000000000
--- a/app/Models/AnalyticsEpisodesByCountryModel.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * Class AnalyticsEpisodesByCountry
- * Model for analytics_episodes_by_country table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class AnalyticsEpisodesByCountryModel extends Model
-{
-    protected $table = 'analytics_episodes_by_country';
-    protected $primaryKey = 'id';
-
-    protected $allowedFields = [];
-
-    protected $returnType = \App\Entities\AnalyticsEpisodesByCountry::class;
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Models/AnalyticsEpisodesByPlayerModel.php b/app/Models/AnalyticsEpisodesByPlayerModel.php
deleted file mode 100644
index efeefb784422adee83cbf3167f7878bd352ce3ae..0000000000000000000000000000000000000000
--- a/app/Models/AnalyticsEpisodesByPlayerModel.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * Class AnalyticsEpisodesByPlayerModel
- * Model for analytics_episodes_by_player table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class AnalyticsEpisodesByPlayerModel extends Model
-{
-    protected $table = 'analytics_episodes_by_player';
-    protected $primaryKey = 'id';
-
-    protected $allowedFields = [];
-
-    protected $returnType = \App\Entities\AnalyticsEpisodesByPlayer::class;
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Models/AnalyticsPodcastsModel.php b/app/Models/AnalyticsPodcastByCountryModel.php
similarity index 51%
rename from app/Models/AnalyticsPodcastsModel.php
rename to app/Models/AnalyticsPodcastByCountryModel.php
index 8ab115a0ea6a38733344ed1748cbbc99eb101cc7..d26b5868451136825a836247de75da32ebd298a7 100644
--- a/app/Models/AnalyticsPodcastsModel.php
+++ b/app/Models/AnalyticsPodcastByCountryModel.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AnalyticsPodcastsModel
- * Model for analytics_podcasts table in database
+ * Class AnalyticsPodcastByCountryModel
+ * Model for analytics_podcasts_by_country table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -12,44 +12,43 @@ namespace App\Models;
 
 use CodeIgniter\Model;
 
-class AnalyticsPodcastsModel extends Model
+class AnalyticsPodcastByCountryModel extends Model
 {
-    protected $table = 'analytics_podcasts';
+    protected $table = 'analytics_podcasts_by_country';
 
     protected $allowedFields = [];
 
-    protected $returnType = \App\Entities\AnalyticsPodcasts::class;
+    protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class;
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
 
     /**
-     * Gets all data for a podcast
+     * Gets country data for a podcast
      *
      * @param int $podcastId
      *
      * @return array
      */
-    public function getDataByDay(int $podcastId): array
+    public function getData(int $podcastId): array
     {
-        if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) {
-            $found = $this->select('`date` as `labels`')
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_country"))) {
+            $found = $this->select('`country_code` as `labels`')
                 ->selectSum('`hits`', '`values`')
+                ->groupBy('`country_code`')
                 ->where([
                     '`podcast_id`' => $podcastId,
-                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
-                ->groupBy('`labels`')
-                ->orderBy('`labels``', 'ASC')
+                ->orderBy('`labels`', 'ASC')
                 ->findAll();
 
             cache()->save(
-                "{$podcastId}_analytics_podcast_by_day",
+                "{$podcastId}_analytics_podcast_by_country",
                 $found,
-                14400
+                600
             );
         }
-
         return $found;
     }
 }
diff --git a/app/Models/AnalyticsPodcastsByEpisodeModel.php b/app/Models/AnalyticsPodcastByEpisodeModel.php
similarity index 96%
rename from app/Models/AnalyticsPodcastsByEpisodeModel.php
rename to app/Models/AnalyticsPodcastByEpisodeModel.php
index 59c82360ddcaa6e0b1d284da93103bbc1d758e48..2dc3e0ee1905457b815c1bfbd0607a6e891df5da 100644
--- a/app/Models/AnalyticsPodcastsByEpisodeModel.php
+++ b/app/Models/AnalyticsPodcastByEpisodeModel.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Class AnalyticsPodcastsByEpisodeModel
+ * Class AnalyticsPodcastByEpisodeModel
  * Model for analytics_podcasts_by_episodes table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@@ -12,7 +12,7 @@ namespace App\Models;
 
 use CodeIgniter\Model;
 
-class AnalyticsPodcastsByEpisodeModel extends Model
+class AnalyticsPodcastByEpisodeModel extends Model
 {
     protected $table = 'analytics_podcasts_by_episode';
 
@@ -81,7 +81,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model
                 cache()->save(
                     "{$podcastId}_analytics_podcast_by_episode_by_day",
                     $found,
-                    14400
+                    600
                 );
             }
             return $found;
@@ -104,7 +104,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model
                 cache()->save(
                     "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
                     $found,
-                    14400
+                    600
                 );
             }
             return $found;
diff --git a/app/Models/AnalyticsPodcastsByPlayerModel.php b/app/Models/AnalyticsPodcastByPlayerModel.php
similarity index 88%
rename from app/Models/AnalyticsPodcastsByPlayerModel.php
rename to app/Models/AnalyticsPodcastByPlayerModel.php
index 900b44fa3929003df4737288e676450853d7ef74..dfe3e4938c791aaa554fe863e0541b6167382116 100644
--- a/app/Models/AnalyticsPodcastsByPlayerModel.php
+++ b/app/Models/AnalyticsPodcastByPlayerModel.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Class AnalyticsPodcastsByPlayerModel
+ * Class AnalyticsPodcastByPlayerModel
  * Model for analytics_podcasts_by_player table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@@ -12,7 +12,7 @@ namespace App\Models;
 
 use CodeIgniter\Model;
 
-class AnalyticsPodcastsByPlayerModel extends Model
+class AnalyticsPodcastByPlayerModel extends Model
 {
     protected $table = 'analytics_podcasts_by_player';
 
@@ -24,7 +24,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
     protected $useTimestamps = false;
 
     /**
-     * Gets all data for a podcast
+     * Gets player data for a podcast
      *
      * @param int $podcastId
      *
@@ -41,18 +41,18 @@ class AnalyticsPodcastsByPlayerModel extends Model
                 ->selectSum('`hits`', '`values`')
                 ->where([
                     '`podcast_id`' => $podcastId,
-                    '`app` !=' => null,
+                    '`app` !=' => '',
                     '`bot`' => 0,
                     '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
                 ->groupBy('`labels`')
-                ->orderBy('`values``', 'DESC')
+                ->orderBy('`values`', 'DESC')
                 ->findAll(10);
 
             cache()->save(
                 "{$podcastId}_analytics_podcasts_by_player_by_app",
                 $found,
-                14400
+                600
             );
         }
 
@@ -60,7 +60,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
     }
 
     /**
-     * Gets all data for a podcast
+     * Gets device data for a podcast
      *
      * @param int $podcastId
      *
@@ -84,7 +84,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
                     '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
                 ->groupBy('`ids`')
-                ->orderBy('`values``', 'DESC')
+                ->orderBy('`values`', 'DESC')
                 ->findAll();
 
             $foundOs = $this->select(
@@ -98,7 +98,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
                     '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
                 ->groupBy('`ids`')
-                ->orderBy('`values``', 'DESC')
+                ->orderBy('`values`', 'DESC')
                 ->findAll();
 
             $foundDevice = $this->select(
@@ -112,7 +112,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
                     '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
                 ->groupBy('`ids`')
-                ->orderBy('`values``', 'DESC')
+                ->orderBy('`values`', 'DESC')
                 ->findAll();
 
             $foundBot = $this->select(
@@ -125,14 +125,14 @@ class AnalyticsPodcastsByPlayerModel extends Model
                     '`date` >' => date('Y-m-d', strtotime('-1 week')),
                 ])
                 ->groupBy('`ids`')
-                ->orderBy('`values``', 'DESC')
+                ->orderBy('`values`', 'DESC')
                 ->findAll();
 
             $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot);
             cache()->save(
                 "{$podcastId}_analytics_podcasts_by_player_by_device",
                 $found,
-                14400
+                600
             );
         }
 
diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Models/AnalyticsPodcastByRegionModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..9953fbe2e05bca3f82f289bd8bc503241bd7a958
--- /dev/null
+++ b/app/Models/AnalyticsPodcastByRegionModel.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastByRegionModel
+ * Model for analytics_podcasts_by_region table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsPodcastByRegionModel extends Model
+{
+    protected $table = 'analytics_podcasts_by_region';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    /**
+     * Gets region data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getData(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_region"))) {
+            $found = $this->select(
+                '`country_code`, `region_code`, `latitude`, `longitude`'
+            )
+                ->selectSum('`hits`', '`values`')
+                ->groupBy(
+                    '`country_code`, `region_code`, `latitude`, `longitude`'
+                )
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->orderBy('`country_code`, `region_code`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_by_region",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+}
diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Models/AnalyticsPodcastModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..82e5711330bd95c69e7d7480db760cec0500837e
--- /dev/null
+++ b/app/Models/AnalyticsPodcastModel.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastModel
+ * Model for analytics_podcasts table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsPodcastModel extends Model
+{
+    protected $table = 'analytics_podcasts';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsPodcasts::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    /**
+     * Gets hits data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByDay(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) {
+            $found = $this->select('`date` as `labels`, `hits` as `values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                ])
+                ->orderBy('`labels`', 'ASC')
+                ->findAll();
+
+            cache()->save("{$podcastId}_analytics_podcast_by_day", $found, 600);
+        }
+        return $found;
+    }
+
+    /**
+     * Gets hits data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByMonth(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_month"))) {
+            $found = $this->select(
+                'concat(year(`date`),"-",month(`date`),"-01") as `labels`'
+            )
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                ])
+                ->orderBy('`date`', 'ASC')
+                ->groupBy('concat(month(`date`),"-",year(`date`))')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_by_month",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
+    /**
+     * Gets unique listeners data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataUniqueListenersByDay(int $podcastId): array
+    {
+        if (
+            !($found = cache(
+                "{$podcastId}_analytics_podcast_unique_listeners_by_day"
+            ))
+        ) {
+            $found = $this->select(
+                '`date` as `labels`, `unique_listeners` as `values`'
+            )
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                ])
+                ->orderBy('`labels`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_unique_listeners_by_day",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
+    /**
+     * Gets unique listeners data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataUniqueListenersByMonth(int $podcastId): array
+    {
+        if (
+            !($found = cache(
+                "{$podcastId}_analytics_podcast_unique_listeners_by_month"
+            ))
+        ) {
+            $found = $this->select(
+                'concat(year(`date`),"-",month(`date`),"-01") as `labels`'
+            )
+                ->selectSum('`unique_listeners`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                ])
+                ->groupBy('concat(month(`date`),"-",year(`date`))')
+                ->orderBy('`date`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_unique_listeners_by_month",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+}
diff --git a/app/Models/AnalyticsPodcastsByCountryModel.php b/app/Models/AnalyticsPodcastsByCountryModel.php
deleted file mode 100644
index 4f2094532ce8d7d4235a4e202d867899be08f7d9..0000000000000000000000000000000000000000
--- a/app/Models/AnalyticsPodcastsByCountryModel.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * Class AnalyticsPodcastsByCountryModel
- * Model for analytics_podcasts_by_country table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class AnalyticsPodcastsByCountryModel extends Model
-{
-    protected $table = 'analytics_podcasts_by_country';
-
-    protected $allowedFields = [];
-
-    protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class;
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Models/AnalyticsPodcastsByRegionModel.php b/app/Models/AnalyticsPodcastsByRegionModel.php
deleted file mode 100644
index 81ab8537f89a322fda5b0b5973c23b2aea736eed..0000000000000000000000000000000000000000
--- a/app/Models/AnalyticsPodcastsByRegionModel.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-
-/**
- * Class AnalyticsPodcastsByRegionModel
- * Model for analytics_podcasts_by_region table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class AnalyticsPodcastsByRegionModel extends Model
-{
-    protected $table = 'analytics_podcasts_by_region';
-
-    protected $allowedFields = [];
-
-    protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class;
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Models/AnalyticsWebsiteByBrowserModel.php
index 85b4fc92fe65d8fde7e19f11d1b4790b5099f84e..4d5c0e280166fa9a1c96b2a89237724a1eb5ccf0 100644
--- a/app/Models/AnalyticsWebsiteByBrowserModel.php
+++ b/app/Models/AnalyticsWebsiteByBrowserModel.php
@@ -22,4 +22,33 @@ class AnalyticsWebsiteByBrowserModel extends Model
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
+
+    /**
+     * Gets browser data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getData(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_website_by_browser"))) {
+            $found = $this->select('`browser` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->groupBy('`browser`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->orderBy('`browser`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_website_by_browser",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
 }
diff --git a/app/Models/AnalyticsWebsiteByCountryModel.php b/app/Models/AnalyticsWebsiteByCountryModel.php
deleted file mode 100644
index 0163b65bfe8bade82ec7e9a762dbe0ed0e9888bc..0000000000000000000000000000000000000000
--- a/app/Models/AnalyticsWebsiteByCountryModel.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * Class AnalyticsWebsiteByCountryModel
- * Model for analytics_website_by_country table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Models;
-
-use CodeIgniter\Model;
-
-class AnalyticsWebsiteByCountryModel extends Model
-{
-    protected $table = 'analytics_website_by_country';
-    protected $primaryKey = 'id';
-
-    protected $allowedFields = [];
-
-    protected $returnType = \App\Entities\AnalyticsWebsiteByCountry::class;
-    protected $useSoftDeletes = false;
-
-    protected $useTimestamps = false;
-}
diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Models/AnalyticsWebsiteByEntryPageModel.php
index 6e7cfa0c9a0e2f31189f79ed3eb744b8ff123e1c..91c4053a1f13375c5082119481d6819c217538fb 100644
--- a/app/Models/AnalyticsWebsiteByEntryPageModel.php
+++ b/app/Models/AnalyticsWebsiteByEntryPageModel.php
@@ -22,4 +22,33 @@ class AnalyticsWebsiteByEntryPageModel extends Model
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
+
+    /**
+     * Gets entry pages data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getData(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_website_by_entry_page"))) {
+            $found = $this->select('`entry_page` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->groupBy('`entry_page`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->orderBy('`entry_page`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_website_by_entry_page",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
 }
diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Models/AnalyticsWebsiteByRefererModel.php
index 5d60298ce1cee9f526a1c0c3cf3bfd0b09d74ab8..e45ea20ba2e7ef89c970a1f89fe57645ccf750c1 100644
--- a/app/Models/AnalyticsWebsiteByRefererModel.php
+++ b/app/Models/AnalyticsWebsiteByRefererModel.php
@@ -22,4 +22,62 @@ class AnalyticsWebsiteByRefererModel extends Model
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
+
+    /**
+     * Gets referer data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getData(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_website_by_referer"))) {
+            $found = $this->select('`referer` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->groupBy('`referer`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->orderBy('`referer`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_website_by_referer",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
+    /**
+     * Gets domain data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByDomain(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_website_by_domain"))) {
+            $found = $this->select('`domain` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->groupBy('`domain`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->orderBy('`domain`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_website_by_domain",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
 }
diff --git a/app/Views/admin/podcast/analytics.php b/app/Views/admin/podcast/analytics.php
index c57559b648bbdeaedc52ed29d8622d87841048b4..c09619f23448c7139dea16c2650137f38163709a 100644
--- a/app/Views/admin/podcast/analytics.php
+++ b/app/Views/admin/podcast/analytics.php
@@ -9,24 +9,76 @@
 <?= $this->endSection() ?>
 
 <?= $this->section('content') ?>
-<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+
+<h2><?= lang('Charts.podcast_by_day') ?></h2>
+<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
     'analytics-data',
     $podcast->id,
-    'PodcastsByPlayer',
-    'ByApp'
+    'Podcast',
+    'ByDay'
 ) ?>"></div>
-<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
+
+<h2><?= lang('Charts.podcast_by_month') ?></h2>
+<div class="h-64" id="by-month-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
     'analytics-data',
     $podcast->id,
-    'Podcasts',
-    'ByDay'
+    'Podcast',
+    'ByMonth'
 ) ?>"></div>
+
+<h2><?= lang('Charts.unique_daily_listeners') ?></h2>
+<div class="h-64" id="by-day-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'Podcast',
+    'UniqueListenersByDay'
+) ?>"></div>
+
+<h2><?= lang('Charts.unique_monthly_listeners') ?></h2>
+<div class="h-64" id="by-month-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'Podcast',
+    'UniqueListenersByMonth'
+) ?>"></div>
+
+<h2><?= lang('Charts.episodes_by_day') ?></h2>
 <div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to(
     'analytics-data',
     $podcast->id,
-    'PodcastsByEpisode',
+    'PodcastByEpisode',
     'ByDay'
 ) ?>"></div>
 
+<h2><?= lang('Charts.by_player') ?></h2>
+<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'PodcastByPlayer',
+    'ByApp'
+) ?>"></div>
+
+<h2><?= lang('Charts.by_browser') ?></h2>
+<div class="h-64" id="by-browser-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+    'analytics-full-data',
+    $podcast->id,
+    'WebsiteByBrowser'
+) ?>"></div>
+
+<h2><?= lang('Charts.by_country') ?></h2>
+<div class="h-64" id="by-country-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+    'analytics-full-data',
+    $podcast->id,
+    'PodcastByCountry'
+) ?>"></div>
+
+<h2><?= lang('Charts.by_domain') ?></h2>
+<div class="h-64" id="by-domain-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'WebsiteByReferer',
+    'ByDomain'
+) ?>"></div>
+
 <script src="/assets/charts.js" type="module"></script>
 <?= $this->endSection() ?>