diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4b364873..9a5a0312 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,13 +39,11 @@ bundle_app:
script:
# build all assets for views
- npm run build
- # download GeoLite2-Country and opawg/user-agents archives and extract them to writable/uploads
- - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/
- - wget -c "https://github.com/opawg/user-agents/archive/master.tar.gz" -O - | tar -xz -C ./writable/uploads/
+ # download GeoLite2-City archive and extract it to writable/uploads
+ - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/
# rename extracted archives' folders
- - mv ./writable/uploads/GeoLite2-Country* ./writable/uploads/GeoLite2-Country
- - mv ./writable/uploads/user-agents* ./writable/uploads/user-agents
+ - mv ./writable/uploads/GeoLite2-City* ./writable/uploads/GeoLite2-City
# create bundle folder: uses .rsync-filter (-F) file to copy only needed files
- rsync -avF --progress . ./bundle
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index df0c8a6e..19486297 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -12,16 +12,20 @@ PHP Dependencies:
- [commonmark](https://commonmark.thephpleague.com/) ([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE))
- [phpdotenv](https://github.com/vlucas/phpdotenv) ([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE))
- [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown) ([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE))
+- [podlibre/user-agents-php](https://github.com/podlibre/user-agents-php) ([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE))
+- [podlibre/ipcat](https://github.com/podlibre/ipcat) ([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE))
Javascript dependencies:
- [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
- [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
- [ProseMirror](https://prosemirror.net/) ([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE))
-- [D3: Data-Driven Documents](https://d3js.org) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE))
+- [amCharts 4](https://github.com/amcharts/amcharts4) ([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE))
- [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
Other:
- [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
-- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
+- [OPAWG/User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
+- [client9/ipcat](https://github.com/client9/ipcat) ([GNU General Public License v3.0](https://github.com/client9/ipcat/blob/master/LICENSE))
+- [GeoLite2 City](https://dev.maxmind.com/geoip/geoip2/geolite2/) ([Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://www.maxmind.com/en/geolite2/eula))
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 3a786da0..5d2c45f8 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -53,10 +53,14 @@ $routes->group(config('App')->installGateway, function ($routes) {
]);
});
-// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
-$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
- 'as' => 'analytics_hit',
-]);
+// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/podcast_folder/filename.mp3)
+$routes->add(
+ 'audio/(:num)/(:num)/(:num)/(:num)/(:any)',
+ 'Analytics::hit/$1/$2/$3/$4/$5',
+ [
+ 'as' => 'analytics_hit',
+ ]
+);
// Show the Unknown UserAgents
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
@@ -113,6 +117,26 @@ $routes->group(
'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete',
]);
+ $routes->get('analytics', 'Podcast::analytics/$1', [
+ 'as' => 'podcast-analytics',
+ 'filter' => 'permission:podcasts-view,podcast-view',
+ ]);
+ $routes->get(
+ 'analytics-data/(:segment)/(:segment)',
+ 'AnalyticsData::getData/$1/$2/$3',
+ [
+ 'as' => 'analytics-data',
+ 'filter' => 'permission:podcasts-view,podcast-view',
+ ]
+ );
+ $routes->get(
+ 'analytics-data/(:segment)/(:segment)/(:num)',
+ 'AnalyticsData::getData/$1/$2/$3/$4',
+ [
+ 'as' => 'analytics-filtered-data',
+ 'filter' => 'permission:podcasts-view,podcast-view',
+ ]
+ );
// Podcast episodes
$routes->group('episodes', function ($routes) {
diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php
new file mode 100644
index 00000000..a57960df
--- /dev/null
+++ b/app/Controllers/Admin/AnalyticsData.php
@@ -0,0 +1,69 @@
+ 2) {
+ 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];
+ if (count($params) > 3) {
+ if (
+ !($this->episode = (new EpisodeModel())
+ ->where([
+ 'podcast_id' => $this->podcast->id,
+ 'id' => $params[3],
+ ])
+ ->first())
+ ) {
+ throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
+ 'Episode not found: ' . $params[3]
+ );
+ }
+ }
+ }
+
+ return $this->$method();
+ }
+ public function getData()
+ {
+ $analytics_model = new $this->className();
+ $methodName = $this->methodName;
+ if ($this->episode) {
+ return $this->response->setJSON(
+ $analytics_model->$methodName(
+ $this->podcast->id,
+ $this->episode->id
+ )
+ );
+ } else {
+ return $this->response->setJSON(
+ $analytics_model->$methodName($this->podcast->id)
+ );
+ }
+ }
+}
diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php
index 439a86da..64594e07 100644
--- a/app/Controllers/Admin/Podcast.php
+++ b/app/Controllers/Admin/Podcast.php
@@ -58,6 +58,14 @@ class Podcast extends BaseController
return view('admin/podcast/view', $data);
}
+ public function analytics()
+ {
+ $data = ['podcast' => $this->podcast];
+
+ replace_breadcrumb_params([0 => $this->podcast->title]);
+ return view('admin/podcast/analytics', $data);
+ }
+
public function create()
{
helper(['form', 'misc']);
@@ -204,7 +212,9 @@ class Podcast extends BaseController
$podcast = new \App\Entities\Podcast([
'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost('imported_feed_url'),
-
+ 'new_feed_url' => base_url(
+ route_to('podcast_feed', $this->request->getPost('name'))
+ ),
'title' => $feed->channel[0]->title,
'description' => $feed->channel[0]->description,
'image' => download_file($nsItunes->image->attributes()),
@@ -214,7 +224,9 @@ class Podcast extends BaseController
? null
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
- : null),
+ : (in_array($nsItunes->explicit, ['no', 'false'])
+ ? 'clean'
+ : null)),
'owner_name' => $nsItunes->owner->name,
'owner_email' => $nsItunes->owner->email,
'publisher' => $nsItunes->author,
@@ -302,11 +314,13 @@ class Podcast extends BaseController
'image' => empty($nsItunes->image->attributes())
? null
: download_file($nsItunes->image->attributes()),
- 'explicit' => $nsItunes->explicit
- ? (in_array($nsItunes->explicit, ['yes', 'true'])
+ 'parental_advisory' => empty($nsItunes->explicit)
+ ? null
+ : (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
- : null)
- : null,
+ : (in_array($nsItunes->explicit, ['no', 'false'])
+ ? 'clean'
+ : null)),
'number' =>
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php
index ec89dc22..3482b1eb 100644
--- a/app/Controllers/Analytics.php
+++ b/app/Controllers/Analytics.php
@@ -40,16 +40,22 @@ class Analytics extends Controller
// E.g.:
// $this->session = \Config\Services::session();
- set_user_session_country();
+ set_user_session_deny_list_ip();
+ set_user_session_location();
set_user_session_player();
}
// Add one hit to this episode:
- public function hit($p_podcastId, $p_episodeId, ...$filename)
- {
+ public function hit(
+ $podcastId,
+ $episodeId,
+ $bytesThreshold,
+ $fileSize,
+ ...$filename
+ ) {
helper('media');
- podcast_hit($p_podcastId, $p_episodeId);
+ podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize);
return redirect()->to(media_url(implode('/', $filename)));
}
}
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 2f5bdcff..3bfbd4b0 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -45,9 +45,10 @@ class BaseController extends Controller
// E.g.:
// $this->session = \Config\Services::session();
- set_user_session_country();
+ set_user_session_deny_list_ip();
set_user_session_browser();
set_user_session_referer();
+ set_user_session_entry_page();
}
protected static function triggerWebpageHit($podcastId)
diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index a95e4db1..018315cf 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -110,6 +110,13 @@ class AddPodcasts extends Migration
'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
+ 'new_feed_url' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 1024,
+ 'comment' =>
+ 'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
+ 'null' => true,
+ ],
'created_at' => [
'type' => 'TIMESTAMP',
],
diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
index 24b2f02f..31334686 100644
--- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php
+++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
@@ -61,6 +61,12 @@ class AddEpisodes extends Migration
'unsigned' => true,
'comment' => 'File size in bytes',
],
+ 'enclosure_headersize' => [
+ 'type' => 'INT',
+ 'constraint' => 10,
+ 'unsigned' => true,
+ 'comment' => 'Header size in bytes',
+ ],
'description' => [
'type' => 'TEXT',
'null' => true,
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
new file mode 100644
index 00000000..e31d932d
--- /dev/null
+++ b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
@@ -0,0 +1,49 @@
+forge->addField([
+ 'podcast_id' => [
+ 'type' => 'BIGINT',
+ 'constraint' => 20,
+ 'unsigned' => true,
+ ],
+ 'date' => [
+ 'type' => 'date',
+ ],
+ 'hits' => [
+ 'type' => 'INT',
+ 'constraint' => 10,
+ 'default' => 1,
+ ],
+ ]);
+ $this->forge->addPrimaryKey(['podcast_id', 'date']);
+ $this->forge->addField(
+ '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
+ );
+ $this->forge->addField(
+ '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+ );
+ $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+ $this->forge->createTable('analytics_podcasts');
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('analytics_podcasts');
+ }
+}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
similarity index 67%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php
rename to app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
index d0e94c6c..b189ef06 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php
+++ b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
@@ -12,34 +12,28 @@ namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
-class AddAnalyticsEpisodesByCountry extends Migration
+class AddAnalyticsPodcastsByEpisode extends Migration
{
public function up()
{
$this->forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
+ 'date' => [
+ 'type' => 'date',
+ ],
'episode_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
- 'country_code' => [
- 'type' => 'VARCHAR',
- 'constraint' => 3,
- 'comment' => 'ISO 3166-1 code.',
- ],
- 'date' => [
- 'type' => 'date',
+ 'age' => [
+ 'type' => 'INT',
+ 'constraint' => 10,
+ 'unsigned' => true,
],
'hits' => [
'type' => 'INT',
@@ -47,13 +41,7 @@ class AddAnalyticsEpisodesByCountry extends Migration
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey([
- 'podcast_id',
- 'episode_id',
- 'country_code',
- 'date',
- ]);
+ $this->forge->addPrimaryKey(['podcast_id', 'episode_id', 'date']);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
@@ -62,11 +50,11 @@ class AddAnalyticsEpisodesByCountry extends Migration
);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
$this->forge->addForeignKey('episode_id', 'episodes', 'id');
- $this->forge->createTable('analytics_episodes_by_country');
+ $this->forge->createTable('analytics_podcasts_by_episode');
}
public function down()
{
- $this->forge->dropTable('analytics_episodes_by_country');
+ $this->forge->dropTable('analytics_podcasts_by_episode');
}
}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
similarity index 70%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php
rename to app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
index 3a13f65f..c1bc04af 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php
+++ b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
@@ -17,32 +17,45 @@ class AddAnalyticsPodcastsByPlayer extends Migration
public function up()
{
$this->forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
- 'player' => [
- 'type' => 'VARCHAR',
- 'constraint' => 191,
- ],
'date' => [
'type' => 'date',
],
+ 'app' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 128,
+ ],
+ 'device' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 32,
+ ],
+ 'os' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 32,
+ ],
+ 'bot' => [
+ 'type' => 'TINYINT',
+ 'constraint' => 1,
+ 'default' => 0,
+ ],
'hits' => [
'type' => 'INT',
'constraint' => 10,
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey(['podcast_id', 'player', 'date']);
+ $this->forge->addPrimaryKey([
+ 'podcast_id',
+ 'app',
+ 'device',
+ 'os',
+ 'bot',
+ 'date',
+ ]);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
similarity index 83%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php
rename to app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
index 6545a7a1..e5f045e6 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php
+++ b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
@@ -17,33 +17,26 @@ class AddAnalyticsPodcastsByCountry extends Migration
public function up()
{
$this->forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
+ 'date' => [
+ 'type' => 'date',
+ ],
'country_code' => [
'type' => 'VARCHAR',
'constraint' => 3,
'comment' => 'ISO 3166-1 code.',
],
- 'date' => [
- 'type' => 'date',
- ],
'hits' => [
'type' => 'INT',
'constraint' => 10,
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
+ $this->forge->addPrimaryKey(['podcast_id', 'country_code', 'date']);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
similarity index 60%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php
rename to app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
index 7f8b1415..7b787878 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php
+++ b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
@@ -1,8 +1,8 @@
forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
+ 'date' => [
+ 'type' => 'date',
+ ],
'country_code' => [
'type' => 'VARCHAR',
'constraint' => 3,
'comment' => 'ISO 3166-1 code.',
],
- 'date' => [
- 'type' => 'date',
+ 'region_code' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 3,
+ 'comment' => 'ISO 3166-2 code.',
+ ],
+ 'latitude' => [
+ 'type' => 'FLOAT',
+ 'null' => true,
+ ],
+ 'longitude' => [
+ 'type' => 'FLOAT',
+ 'null' => true,
],
'hits' => [
'type' => 'INT',
@@ -42,8 +49,12 @@ class AddAnalyticsWebsiteByCountry extends Migration
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
+ $this->forge->addPrimaryKey([
+ 'podcast_id',
+ 'country_code',
+ 'region_code',
+ 'date',
+ ]);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
@@ -51,11 +62,11 @@ class AddAnalyticsWebsiteByCountry extends Migration
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
- $this->forge->createTable('analytics_website_by_country');
+ $this->forge->createTable('analytics_podcasts_by_region');
}
public function down()
{
- $this->forge->dropTable('analytics_website_by_country');
+ $this->forge->dropTable('analytics_podcasts_by_region');
}
}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
similarity index 82%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php
rename to app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
index 6e4942d4..21724af7 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php
+++ b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
@@ -17,32 +17,26 @@ class AddAnalyticsWebsiteByBrowser extends Migration
public function up()
{
$this->forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
+ 'date' => [
+ 'type' => 'date',
+ ],
'browser' => [
'type' => 'VARCHAR',
'constraint' => 191,
],
- 'date' => [
- 'type' => 'date',
- ],
+
'hits' => [
'type' => 'INT',
'constraint' => 10,
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey(['podcast_id', 'browser', 'date']);
+ $this->forge->addPrimaryKey(['podcast_id', 'browser', 'date']);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
similarity index 78%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php
rename to app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
index 28808f27..579024b0 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php
+++ b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
@@ -17,33 +17,36 @@ class AddAnalyticsWebsiteByReferer extends Migration
public function up()
{
$this->forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
'podcast_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
],
- 'referer' => [
- 'type' => 'VARCHAR',
- 'constraint' => 191,
- 'comment' => 'Referer URL.',
- ],
'date' => [
'type' => 'date',
],
+ 'referer' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 512,
+ 'comment' => 'Referer URL.',
+ ],
+ 'domain' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 128,
+ 'null' => true,
+ ],
+ 'keywords' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 384,
+ 'null' => true,
+ ],
'hits' => [
'type' => 'INT',
'constraint' => 10,
'default' => 1,
],
]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey(['podcast_id', 'referer', 'date']);
+ $this->forge->addPrimaryKey(['podcast_id', 'referer', 'date']);
$this->forge->addField(
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
);
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
new file mode 100644
index 00000000..19bce6de
--- /dev/null
+++ b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
@@ -0,0 +1,54 @@
+forge->addField([
+ 'podcast_id' => [
+ 'type' => 'BIGINT',
+ 'constraint' => 20,
+ 'unsigned' => true,
+ ],
+ 'date' => [
+ 'type' => 'date',
+ ],
+ 'entry_page' => [
+ 'type' => 'VARCHAR',
+ 'constraint' => 512,
+ 'comment' => 'Entry page URL.',
+ ],
+ 'hits' => [
+ 'type' => 'INT',
+ 'constraint' => 10,
+ 'default' => 1,
+ ],
+ ]);
+ $this->forge->addPrimaryKey(['podcast_id', 'entry_page', 'date']);
+ $this->forge->addField(
+ '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
+ );
+ $this->forge->addField(
+ '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+ );
+ $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+ $this->forge->createTable('analytics_website_by_entry_page');
+ }
+
+ public function down()
+ {
+ $this->forge->dropTable('analytics_website_by_entry_page');
+ }
+}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php b/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php
deleted file mode 100644
index 3a1e257a..00000000
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php
+++ /dev/null
@@ -1,71 +0,0 @@
-forge->addField([
- 'id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- 'auto_increment' => true,
- ],
- 'podcast_id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- ],
- 'episode_id' => [
- 'type' => 'BIGINT',
- 'constraint' => 20,
- 'unsigned' => true,
- ],
- 'player' => [
- 'type' => 'VARCHAR',
- 'constraint' => 191,
- ],
- 'date' => [
- 'type' => 'date',
- ],
- 'hits' => [
- 'type' => 'INT',
- 'constraint' => 10,
- 'default' => 1,
- ],
- ]);
- $this->forge->addKey('id', true);
- $this->forge->addUniqueKey([
- 'podcast_id',
- 'episode_id',
- 'player',
- 'date',
- ]);
- $this->forge->addField(
- '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
- );
- $this->forge->addField(
- '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
- );
- $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
- $this->forge->addForeignKey('episode_id', 'episodes', 'id');
- $this->forge->createTable('analytics_episodes_by_player');
- }
-
- public function down()
- {
- $this->forge->dropTable('analytics_episodes_by_player');
- }
-}
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 18bd203a..caf35500 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
@@ -18,26 +18,42 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration
{
// Creates Stored Procedure for data insertion
// Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer');
- $procedureName = $this->db->prefixTable('analytics_podcasts');
- $episodesTableName = $this->db->prefixTable('analytics_episodes');
+ $prefix = $this->db->getPrefix();
+
$createQuery = <<
Copying and broadcasting a podcast without the proper rights is piracy and is liable to prosecution.',
+ 'warning_title' => 'Warning',
+ 'warning_content' =>
+ 'This procedure may take a long time.
The current version does not show any progress while it runs. You will not see anything updated until it is done.
In case of timeout error, increase max_execution_time value.',
'old_podcast_section_title' => 'The podcast to import',
'old_podcast_section_subtitle' => '',
'imported_feed_url' => 'Feed URL',
diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php
index 05fe31f1..3d153206 100644
--- a/app/Language/en/PodcastNavigation.php
+++ b/app/Language/en/PodcastNavigation.php
@@ -20,4 +20,5 @@ return [
'contributor-add' => 'Add contributor',
'settings' => 'Settings',
'platforms' => 'Podcast platforms',
+ 'podcast-analytics' => 'Audiences Overview',
];
diff --git a/app/Models/AnalyticsPodcastsByCountryModel.php b/app/Models/AnalyticsPodcastsByCountryModel.php
index 70f5fc3e..4f209453 100644
--- a/app/Models/AnalyticsPodcastsByCountryModel.php
+++ b/app/Models/AnalyticsPodcastsByCountryModel.php
@@ -2,7 +2,7 @@
/**
* Class AnalyticsPodcastsByCountryModel
- * Model for analytics_episodes_by_country table in database
+ * 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/
@@ -14,8 +14,7 @@ use CodeIgniter\Model;
class AnalyticsPodcastsByCountryModel extends Model
{
- protected $table = 'analytics_episodes_by_country';
- protected $primaryKey = 'id';
+ protected $table = 'analytics_podcasts_by_country';
protected $allowedFields = [];
diff --git a/app/Models/AnalyticsPodcastsByEpisodeModel.php b/app/Models/AnalyticsPodcastsByEpisodeModel.php
new file mode 100644
index 00000000..59c82360
--- /dev/null
+++ b/app/Models/AnalyticsPodcastsByEpisodeModel.php
@@ -0,0 +1,113 @@
+select('id, season_number, number, title')
+ ->orderBy('id', 'DESC')
+ ->where(['podcast_id' => $podcastId])
+ ->findAll(5);
+
+ $found = $this->select('age AS X');
+
+ $letter = 97;
+ foreach ($lastEpisodes as $episode) {
+ $found = $found
+ ->selectSum(
+ '(CASE WHEN `episode_id`=' .
+ $episode->id .
+ ' THEN `hits` END)',
+ chr($letter) . 'Y'
+ )
+ ->select(
+ '"' .
+ (empty($episode->season_number)
+ ? ''
+ : $episode->season_number) .
+ (empty($episode->number)
+ ? ''
+ : '-' . $episode->number . '/ ') .
+ $episode->title .
+ '" AS ' .
+ chr($letter) .
+ 'Value'
+ );
+ $letter++;
+ }
+
+ $found = $found
+ ->where([
+ 'podcast_id' => $podcastId,
+ 'age <' => 60,
+ ])
+ ->groupBy('X')
+ ->orderBy('X', 'ASC')
+ ->findAll();
+
+ cache()->save(
+ "{$podcastId}_analytics_podcast_by_episode_by_day",
+ $found,
+ 14400
+ );
+ }
+ return $found;
+ } else {
+ if (
+ !($found = cache(
+ "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day"
+ ))
+ ) {
+ $found = $this->select('date as labels')
+ ->selectSum('hits', 'values')
+ ->where([
+ 'episode_id' => $episodeId,
+ 'podcast_id' => $podcastId,
+ ])
+ ->groupBy('labels')
+ ->orderBy('labels', 'ASC')
+ ->findAll();
+
+ cache()->save(
+ "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
+ $found,
+ 14400
+ );
+ }
+ return $found;
+ }
+ }
+}
diff --git a/app/Models/AnalyticsPodcastsByPlayerModel.php b/app/Models/AnalyticsPodcastsByPlayerModel.php
index 5e0ff822..900b44fa 100644
--- a/app/Models/AnalyticsPodcastsByPlayerModel.php
+++ b/app/Models/AnalyticsPodcastsByPlayerModel.php
@@ -15,7 +15,6 @@ use CodeIgniter\Model;
class AnalyticsPodcastsByPlayerModel extends Model
{
protected $table = 'analytics_podcasts_by_player';
- protected $primaryKey = 'id';
protected $allowedFields = [];
@@ -23,4 +22,120 @@ class AnalyticsPodcastsByPlayerModel extends Model
protected $useSoftDeletes = false;
protected $useTimestamps = false;
+
+ /**
+ * Gets all data for a podcast
+ *
+ * @param int $podcastId
+ *
+ * @return array
+ */
+ public function getDataByApp(int $podcastId): array
+ {
+ if (
+ !($found = cache(
+ "{$podcastId}_analytics_podcasts_by_player_by_app"
+ ))
+ ) {
+ $found = $this->select('`app` as `labels`')
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`app` !=' => null,
+ '`bot`' => 0,
+ '`date` >' => date('Y-m-d', strtotime('-1 week')),
+ ])
+ ->groupBy('`labels`')
+ ->orderBy('`values``', 'DESC')
+ ->findAll(10);
+
+ cache()->save(
+ "{$podcastId}_analytics_podcasts_by_player_by_app",
+ $found,
+ 14400
+ );
+ }
+
+ return $found;
+ }
+
+ /**
+ * Gets all data for a podcast
+ *
+ * @param int $podcastId
+ *
+ * @return array
+ */
+ public function getDataByDevice(int $podcastId): array
+ {
+ if (
+ !($found = cache(
+ "{$podcastId}_analytics_podcasts_by_player_by_device"
+ ))
+ ) {
+ $foundApp = $this->select(
+ 'CONCAT_WS("/", `device`, `os`, `app`) as `ids`, `app` as `labels`, CONCAT_WS("/", `device`, `os`) as `parents`'
+ )
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`app` !=' => null,
+ '`bot`' => 0,
+ '`date` >' => date('Y-m-d', strtotime('-1 week')),
+ ])
+ ->groupBy('`ids`')
+ ->orderBy('`values``', 'DESC')
+ ->findAll();
+
+ $foundOs = $this->select(
+ 'CONCAT_WS("/", `device`, `os`) as `ids`, `os` as `labels`, `device` as `parents`'
+ )
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`os` !=' => null,
+ '`bot`' => 0,
+ '`date` >' => date('Y-m-d', strtotime('-1 week')),
+ ])
+ ->groupBy('`ids`')
+ ->orderBy('`values``', 'DESC')
+ ->findAll();
+
+ $foundDevice = $this->select(
+ '`device` as `ids`, `device` as `labels`, "" as `parents`'
+ )
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`device` !=' => null,
+ '`bot`' => 0,
+ '`date` >' => date('Y-m-d', strtotime('-1 week')),
+ ])
+ ->groupBy('`ids`')
+ ->orderBy('`values``', 'DESC')
+ ->findAll();
+
+ $foundBot = $this->select(
+ '"bots" as `ids`, "Bots" as `labels`, "" as `parents`'
+ )
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`bot`' => 1,
+ '`date` >' => date('Y-m-d', strtotime('-1 week')),
+ ])
+ ->groupBy('`ids`')
+ ->orderBy('`values``', 'DESC')
+ ->findAll();
+
+ $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot);
+ cache()->save(
+ "{$podcastId}_analytics_podcasts_by_player_by_device",
+ $found,
+ 14400
+ );
+ }
+
+ return $found;
+ }
}
diff --git a/app/Models/AnalyticsPodcastsByRegionModel.php b/app/Models/AnalyticsPodcastsByRegionModel.php
new file mode 100644
index 00000000..81ab8537
--- /dev/null
+++ b/app/Models/AnalyticsPodcastsByRegionModel.php
@@ -0,0 +1,25 @@
+select('`date` as `labels`')
+ ->selectSum('`hits`', '`values`')
+ ->where([
+ '`podcast_id`' => $podcastId,
+ '`date` >' => date('Y-m-d', strtotime('-1 year')),
+ ])
+ ->groupBy('`labels`')
+ ->orderBy('`labels``', 'ASC')
+ ->findAll();
+
+ cache()->save(
+ "{$podcastId}_analytics_podcast_by_day",
+ $found,
+ 14400
+ );
+ }
+
+ return $found;
+ }
+}
diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Models/AnalyticsWebsiteByBrowserModel.php
index ceee6b3e..85b4fc92 100644
--- a/app/Models/AnalyticsWebsiteByBrowserModel.php
+++ b/app/Models/AnalyticsWebsiteByBrowserModel.php
@@ -15,7 +15,6 @@ use CodeIgniter\Model;
class AnalyticsWebsiteByBrowserModel extends Model
{
protected $table = 'analytics_website_by_browser';
- protected $primaryKey = 'id';
protected $allowedFields = [];
diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Models/AnalyticsWebsiteByEntryPageModel.php
new file mode 100644
index 00000000..6e7cfa0c
--- /dev/null
+++ b/app/Models/AnalyticsWebsiteByEntryPageModel.php
@@ -0,0 +1,25 @@
+
+
diff --git a/app/Views/_assets/images/logo-castopod.svg b/app/Views/_assets/images/logo-castopod.svg
index 191b6cc9..0208232a 100644
--- a/app/Views/_assets/images/logo-castopod.svg
+++ b/app/Views/_assets/images/logo-castopod.svg
@@ -1,86 +1,40 @@
-
-