feat: update analytics so to meet IABv2 requirements
- https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf - no IP address is ever stored on the server. Only aggregate data is stored in the dababase. - rolling 24-hour window - castopod does not do pre-load - IP Blacklisting https://github.com/client9/ipcat - user-agent Filtering https://github.com/opawg/user-agents - ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app) - in case of partial content, adds up all requests to check >1mn was downloaded - identifying Uniques is done with a combination of IP Address and User Agent - add AMcharts - add some graphs - add regions to analytics - add ipcat blacklist - enhance useragents performances - add filesize and header size in order to calculate 1mn downloads - update publisher ID3 field - update castopod icon - add disclaimer and warning import form translation - update docs/setup-development.md closes #10
@ -39,13 +39,11 @@ bundle_app:
|
|||||||
script:
|
script:
|
||||||
# build all assets for views
|
# build all assets for views
|
||||||
- npm run build
|
- npm run build
|
||||||
# download GeoLite2-Country and opawg/user-agents archives and extract them to writable/uploads
|
# download GeoLite2-City archive and extract it 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://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&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/
|
|
||||||
|
|
||||||
# rename extracted archives' folders
|
# rename extracted archives' folders
|
||||||
- mv ./writable/uploads/GeoLite2-Country* ./writable/uploads/GeoLite2-Country
|
- mv ./writable/uploads/GeoLite2-City* ./writable/uploads/GeoLite2-City
|
||||||
- mv ./writable/uploads/user-agents* ./writable/uploads/user-agents
|
|
||||||
|
|
||||||
# create bundle folder: uses .rsync-filter (-F) file to copy only needed files
|
# create bundle folder: uses .rsync-filter (-F) file to copy only needed files
|
||||||
- rsync -avF --progress . ./bundle
|
- rsync -avF --progress . ./bundle
|
||||||
|
@ -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))
|
- [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))
|
- [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))
|
- [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:
|
Javascript dependencies:
|
||||||
|
|
||||||
- [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
|
- [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))
|
- [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))
|
- [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))
|
- [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
|
||||||
|
|
||||||
Other:
|
Other:
|
||||||
|
|
||||||
- [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
|
- [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))
|
||||||
|
@ -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)
|
// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/podcast_folder/filename.mp3)
|
||||||
$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
|
$routes->add(
|
||||||
'as' => 'analytics_hit',
|
'audio/(:num)/(:num)/(:num)/(:num)/(:any)',
|
||||||
]);
|
'Analytics::hit/$1/$2/$3/$4/$5',
|
||||||
|
[
|
||||||
|
'as' => 'analytics_hit',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
// Show the Unknown UserAgents
|
// Show the Unknown UserAgents
|
||||||
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
|
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
|
||||||
@ -113,6 +117,26 @@ $routes->group(
|
|||||||
'as' => 'podcast-delete',
|
'as' => 'podcast-delete',
|
||||||
'filter' => 'permission:podcasts-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
|
// Podcast episodes
|
||||||
$routes->group('episodes', function ($routes) {
|
$routes->group('episodes', function ($routes) {
|
||||||
|
69
app/Controllers/Admin/AnalyticsData.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Models\PodcastModel;
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
|
||||||
|
class AnalyticsData extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \App\Entities\Podcast|null
|
||||||
|
*/
|
||||||
|
protected $podcast;
|
||||||
|
protected $className;
|
||||||
|
protected $methodName;
|
||||||
|
protected $episode;
|
||||||
|
|
||||||
|
public function _remap($method, ...$params)
|
||||||
|
{
|
||||||
|
if (count($params) > 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -58,6 +58,14 @@ class Podcast extends BaseController
|
|||||||
return view('admin/podcast/view', $data);
|
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()
|
public function create()
|
||||||
{
|
{
|
||||||
helper(['form', 'misc']);
|
helper(['form', 'misc']);
|
||||||
@ -204,7 +212,9 @@ class Podcast extends BaseController
|
|||||||
$podcast = new \App\Entities\Podcast([
|
$podcast = new \App\Entities\Podcast([
|
||||||
'name' => $this->request->getPost('name'),
|
'name' => $this->request->getPost('name'),
|
||||||
'imported_feed_url' => $this->request->getPost('imported_feed_url'),
|
'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,
|
'title' => $feed->channel[0]->title,
|
||||||
'description' => $feed->channel[0]->description,
|
'description' => $feed->channel[0]->description,
|
||||||
'image' => download_file($nsItunes->image->attributes()),
|
'image' => download_file($nsItunes->image->attributes()),
|
||||||
@ -214,7 +224,9 @@ class Podcast extends BaseController
|
|||||||
? null
|
? null
|
||||||
: (in_array($nsItunes->explicit, ['yes', 'true'])
|
: (in_array($nsItunes->explicit, ['yes', 'true'])
|
||||||
? 'explicit'
|
? 'explicit'
|
||||||
: null),
|
: (in_array($nsItunes->explicit, ['no', 'false'])
|
||||||
|
? 'clean'
|
||||||
|
: null)),
|
||||||
'owner_name' => $nsItunes->owner->name,
|
'owner_name' => $nsItunes->owner->name,
|
||||||
'owner_email' => $nsItunes->owner->email,
|
'owner_email' => $nsItunes->owner->email,
|
||||||
'publisher' => $nsItunes->author,
|
'publisher' => $nsItunes->author,
|
||||||
@ -302,11 +314,13 @@ class Podcast extends BaseController
|
|||||||
'image' => empty($nsItunes->image->attributes())
|
'image' => empty($nsItunes->image->attributes())
|
||||||
? null
|
? null
|
||||||
: download_file($nsItunes->image->attributes()),
|
: download_file($nsItunes->image->attributes()),
|
||||||
'explicit' => $nsItunes->explicit
|
'parental_advisory' => empty($nsItunes->explicit)
|
||||||
? (in_array($nsItunes->explicit, ['yes', 'true'])
|
? null
|
||||||
|
: (in_array($nsItunes->explicit, ['yes', 'true'])
|
||||||
? 'explicit'
|
? 'explicit'
|
||||||
: null)
|
: (in_array($nsItunes->explicit, ['no', 'false'])
|
||||||
: null,
|
? 'clean'
|
||||||
|
: null)),
|
||||||
'number' =>
|
'number' =>
|
||||||
$this->request->getPost('force_renumber') === 'yes'
|
$this->request->getPost('force_renumber') === 'yes'
|
||||||
? $itemNumber
|
? $itemNumber
|
||||||
|
@ -40,16 +40,22 @@ class Analytics extends Controller
|
|||||||
// E.g.:
|
// E.g.:
|
||||||
// $this->session = \Config\Services::session();
|
// $this->session = \Config\Services::session();
|
||||||
|
|
||||||
set_user_session_country();
|
set_user_session_deny_list_ip();
|
||||||
|
set_user_session_location();
|
||||||
set_user_session_player();
|
set_user_session_player();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add one hit to this episode:
|
// Add one hit to this episode:
|
||||||
public function hit($p_podcastId, $p_episodeId, ...$filename)
|
public function hit(
|
||||||
{
|
$podcastId,
|
||||||
|
$episodeId,
|
||||||
|
$bytesThreshold,
|
||||||
|
$fileSize,
|
||||||
|
...$filename
|
||||||
|
) {
|
||||||
helper('media');
|
helper('media');
|
||||||
|
|
||||||
podcast_hit($p_podcastId, $p_episodeId);
|
podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize);
|
||||||
return redirect()->to(media_url(implode('/', $filename)));
|
return redirect()->to(media_url(implode('/', $filename)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,10 @@ class BaseController extends Controller
|
|||||||
// E.g.:
|
// E.g.:
|
||||||
// $this->session = \Config\Services::session();
|
// $this->session = \Config\Services::session();
|
||||||
|
|
||||||
set_user_session_country();
|
set_user_session_deny_list_ip();
|
||||||
set_user_session_browser();
|
set_user_session_browser();
|
||||||
set_user_session_referer();
|
set_user_session_referer();
|
||||||
|
set_user_session_entry_page();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function triggerWebpageHit($podcastId)
|
protected static function triggerWebpageHit($podcastId)
|
||||||
|
@ -110,6 +110,13 @@ class AddPodcasts extends Migration
|
|||||||
'The RSS feed URL if this podcast was imported, NULL otherwise.',
|
'The RSS feed URL if this podcast was imported, NULL otherwise.',
|
||||||
'null' => true,
|
'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' => [
|
'created_at' => [
|
||||||
'type' => 'TIMESTAMP',
|
'type' => 'TIMESTAMP',
|
||||||
],
|
],
|
||||||
|
@ -61,6 +61,12 @@ class AddEpisodes extends Migration
|
|||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
'comment' => 'File size in bytes',
|
'comment' => 'File size in bytes',
|
||||||
],
|
],
|
||||||
|
'enclosure_headersize' => [
|
||||||
|
'type' => 'INT',
|
||||||
|
'constraint' => 10,
|
||||||
|
'unsigned' => true,
|
||||||
|
'comment' => 'Header size in bytes',
|
||||||
|
],
|
||||||
'description' => [
|
'description' => [
|
||||||
'type' => 'TEXT',
|
'type' => 'TEXT',
|
||||||
'null' => true,
|
'null' => true,
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AddAnalyticsPodcastsByCountry
|
||||||
|
* Creates 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\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddAnalyticsPodcasts extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
@ -12,34 +12,28 @@ namespace App\Database\Migrations;
|
|||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
class AddAnalyticsEpisodesByCountry extends Migration
|
class AddAnalyticsPodcastsByEpisode extends Migration
|
||||||
{
|
{
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
|
'date' => [
|
||||||
|
'type' => 'date',
|
||||||
|
],
|
||||||
'episode_id' => [
|
'episode_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
'country_code' => [
|
'age' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'INT',
|
||||||
'constraint' => 3,
|
'constraint' => 10,
|
||||||
'comment' => 'ISO 3166-1 code.',
|
'unsigned' => true,
|
||||||
],
|
|
||||||
'date' => [
|
|
||||||
'type' => 'date',
|
|
||||||
],
|
],
|
||||||
'hits' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
@ -47,13 +41,7 @@ class AddAnalyticsEpisodesByCountry extends Migration
|
|||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey(['podcast_id', 'episode_id', 'date']);
|
||||||
$this->forge->addUniqueKey([
|
|
||||||
'podcast_id',
|
|
||||||
'episode_id',
|
|
||||||
'country_code',
|
|
||||||
'date',
|
|
||||||
]);
|
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`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('podcast_id', 'podcasts', 'id');
|
||||||
$this->forge->addForeignKey('episode_id', 'episodes', '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()
|
public function down()
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('analytics_episodes_by_country');
|
$this->forge->dropTable('analytics_podcasts_by_episode');
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,32 +17,45 @@ class AddAnalyticsPodcastsByPlayer extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
'player' => [
|
|
||||||
'type' => 'VARCHAR',
|
|
||||||
'constraint' => 191,
|
|
||||||
],
|
|
||||||
'date' => [
|
'date' => [
|
||||||
'type' => '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' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
'constraint' => 10,
|
'constraint' => 10,
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey([
|
||||||
$this->forge->addUniqueKey(['podcast_id', 'player', 'date']);
|
'podcast_id',
|
||||||
|
'app',
|
||||||
|
'device',
|
||||||
|
'os',
|
||||||
|
'bot',
|
||||||
|
'date',
|
||||||
|
]);
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||||
);
|
);
|
@ -17,33 +17,26 @@ class AddAnalyticsPodcastsByCountry extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
|
'date' => [
|
||||||
|
'type' => 'date',
|
||||||
|
],
|
||||||
'country_code' => [
|
'country_code' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 3,
|
'constraint' => 3,
|
||||||
'comment' => 'ISO 3166-1 code.',
|
'comment' => 'ISO 3166-1 code.',
|
||||||
],
|
],
|
||||||
'date' => [
|
|
||||||
'type' => 'date',
|
|
||||||
],
|
|
||||||
'hits' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
'constraint' => 10,
|
'constraint' => 10,
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey(['podcast_id', 'country_code', 'date']);
|
||||||
$this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
|
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||||
);
|
);
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AddAnalyticsWebsiteByCountry
|
* Class AddAnalyticsPodcastsByRegion
|
||||||
* Creates analytics_website_by_country table in database
|
* Creates analytics_podcasts_by_region table in database
|
||||||
* @copyright 2020 Podlibre
|
* @copyright 2020 Podlibre
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
@ -12,29 +12,36 @@ namespace App\Database\Migrations;
|
|||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
class AddAnalyticsWebsiteByCountry extends Migration
|
class AddAnalyticsPodcastsByRegion extends Migration
|
||||||
{
|
{
|
||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
|
'date' => [
|
||||||
|
'type' => 'date',
|
||||||
|
],
|
||||||
'country_code' => [
|
'country_code' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 3,
|
'constraint' => 3,
|
||||||
'comment' => 'ISO 3166-1 code.',
|
'comment' => 'ISO 3166-1 code.',
|
||||||
],
|
],
|
||||||
'date' => [
|
'region_code' => [
|
||||||
'type' => 'date',
|
'type' => 'VARCHAR',
|
||||||
|
'constraint' => 3,
|
||||||
|
'comment' => 'ISO 3166-2 code.',
|
||||||
|
],
|
||||||
|
'latitude' => [
|
||||||
|
'type' => 'FLOAT',
|
||||||
|
'null' => true,
|
||||||
|
],
|
||||||
|
'longitude' => [
|
||||||
|
'type' => 'FLOAT',
|
||||||
|
'null' => true,
|
||||||
],
|
],
|
||||||
'hits' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
@ -42,8 +49,12 @@ class AddAnalyticsWebsiteByCountry extends Migration
|
|||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey([
|
||||||
$this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
|
'podcast_id',
|
||||||
|
'country_code',
|
||||||
|
'region_code',
|
||||||
|
'date',
|
||||||
|
]);
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`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()'
|
'`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
|
||||||
);
|
);
|
||||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
|
||||||
$this->forge->createTable('analytics_website_by_country');
|
$this->forge->createTable('analytics_podcasts_by_region');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
$this->forge->dropTable('analytics_website_by_country');
|
$this->forge->dropTable('analytics_podcasts_by_region');
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,32 +17,26 @@ class AddAnalyticsWebsiteByBrowser extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
|
'date' => [
|
||||||
|
'type' => 'date',
|
||||||
|
],
|
||||||
'browser' => [
|
'browser' => [
|
||||||
'type' => 'VARCHAR',
|
'type' => 'VARCHAR',
|
||||||
'constraint' => 191,
|
'constraint' => 191,
|
||||||
],
|
],
|
||||||
'date' => [
|
|
||||||
'type' => 'date',
|
|
||||||
],
|
|
||||||
'hits' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
'constraint' => 10,
|
'constraint' => 10,
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey(['podcast_id', 'browser', 'date']);
|
||||||
$this->forge->addUniqueKey(['podcast_id', 'browser', 'date']);
|
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||||
);
|
);
|
@ -17,33 +17,36 @@ class AddAnalyticsWebsiteByReferer extends Migration
|
|||||||
public function up()
|
public function up()
|
||||||
{
|
{
|
||||||
$this->forge->addField([
|
$this->forge->addField([
|
||||||
'id' => [
|
|
||||||
'type' => 'BIGINT',
|
|
||||||
'constraint' => 20,
|
|
||||||
'unsigned' => true,
|
|
||||||
'auto_increment' => true,
|
|
||||||
],
|
|
||||||
'podcast_id' => [
|
'podcast_id' => [
|
||||||
'type' => 'BIGINT',
|
'type' => 'BIGINT',
|
||||||
'constraint' => 20,
|
'constraint' => 20,
|
||||||
'unsigned' => true,
|
'unsigned' => true,
|
||||||
],
|
],
|
||||||
'referer' => [
|
|
||||||
'type' => 'VARCHAR',
|
|
||||||
'constraint' => 191,
|
|
||||||
'comment' => 'Referer URL.',
|
|
||||||
],
|
|
||||||
'date' => [
|
'date' => [
|
||||||
'type' => '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' => [
|
'hits' => [
|
||||||
'type' => 'INT',
|
'type' => 'INT',
|
||||||
'constraint' => 10,
|
'constraint' => 10,
|
||||||
'default' => 1,
|
'default' => 1,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
$this->forge->addKey('id', true);
|
$this->forge->addPrimaryKey(['podcast_id', 'referer', 'date']);
|
||||||
$this->forge->addUniqueKey(['podcast_id', 'referer', 'date']);
|
|
||||||
$this->forge->addField(
|
$this->forge->addField(
|
||||||
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
'`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
|
||||||
);
|
);
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AddAnalyticsWebsiteByReferer
|
||||||
|
* Creates analytics_website_by_referer table in database
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class AddAnalyticsWebsiteByEntryPage extends Migration
|
||||||
|
{
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
@ -1,71 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class AddAnalyticsEpisodesByPlayer
|
|
||||||
* Creates 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\Database\Migrations;
|
|
||||||
|
|
||||||
use CodeIgniter\Database\Migration;
|
|
||||||
|
|
||||||
class AddAnalyticsEpisodesByPlayer 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,
|
|
||||||
],
|
|
||||||
'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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,26 +18,42 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration
|
|||||||
{
|
{
|
||||||
// Creates Stored Procedure for data insertion
|
// Creates Stored Procedure for data insertion
|
||||||
// Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer');
|
// Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer');
|
||||||
$procedureName = $this->db->prefixTable('analytics_podcasts');
|
$prefix = $this->db->getPrefix();
|
||||||
$episodesTableName = $this->db->prefixTable('analytics_episodes');
|
|
||||||
$createQuery = <<<EOD
|
$createQuery = <<<EOD
|
||||||
CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_episode_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_player` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA
|
CREATE PROCEDURE `{$prefix}analytics_podcasts` (
|
||||||
|
IN `p_podcast_id` BIGINT(20) UNSIGNED,
|
||||||
|
IN `p_episode_id` BIGINT(20) UNSIGNED,
|
||||||
|
IN `p_country_code` VARCHAR(3) CHARSET utf8mb4,
|
||||||
|
IN `p_region_code` VARCHAR(3) CHARSET utf8mb4,
|
||||||
|
IN `p_latitude` FLOAT,
|
||||||
|
IN `p_longitude` FLOAT,
|
||||||
|
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
|
||||||
|
) MODIFIES SQL DATA
|
||||||
DETERMINISTIC
|
DETERMINISTIC
|
||||||
SQL SECURITY INVOKER
|
SQL SECURITY INVOKER
|
||||||
COMMENT 'Add one hit in podcast logs tables.'
|
COMMENT 'Add one hit in podcast logs tables.'
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO `{$procedureName}_by_country`(`podcast_id`, `country_code`, `date`)
|
IF NOT `p_bot` THEN
|
||||||
VALUES (p_podcast_id, p_country_code, DATE(NOW()))
|
INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`)
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
VALUES (p_podcast_id, DATE(NOW()))
|
||||||
INSERT INTO `{$procedureName}_by_player`(`podcast_id`, `player`, `date`)
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
VALUES (p_podcast_id, p_player, DATE(NOW()))
|
INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`)
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
SELECT p_podcast_id, p_episode_id, DATE(NOW()), datediff(now(),`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id
|
||||||
INSERT INTO `{$episodesTableName}_by_country`(`podcast_id`, `episode_id`, `country_code`, `date`)
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
VALUES (p_podcast_id, p_episode_id, p_country_code, DATE(NOW()))
|
INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`)
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
VALUES (p_podcast_id, p_country_code, DATE(NOW()))
|
||||||
INSERT INTO `{$episodesTableName}_by_player`(`podcast_id`, `episode_id`, `player`, `date`)
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
VALUES (p_podcast_id, p_episode_id, p_player, DATE(NOW()))
|
INSERT INTO `{$prefix}analytics_podcasts_by_region`(`podcast_id`, `country_code`, `region_code`, `latitude`, `longitude`, `date`)
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
VALUES (p_podcast_id, p_country_code, p_region_code, p_latitude, p_longitude, DATE(NOW()))
|
||||||
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
|
END IF;
|
||||||
|
INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `app`, `device`, `os`, `bot`, `date`)
|
||||||
|
VALUES (p_podcast_id, p_app, p_device, p_os, p_bot, DATE(NOW()))
|
||||||
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
END
|
END
|
||||||
EOD;
|
EOD;
|
||||||
$this->db->query($createQuery);
|
$this->db->query($createQuery);
|
||||||
@ -45,7 +61,9 @@ EOD;
|
|||||||
|
|
||||||
public function down()
|
public function down()
|
||||||
{
|
{
|
||||||
$procedureName = $this->db->prefixTable('analytics_podcasts');
|
$prefix = $this->db->getPrefix();
|
||||||
$this->db->query("DROP PROCEDURE IF EXISTS `$procedureName`");
|
$this->db->query(
|
||||||
|
"DROP PROCEDURE IF EXISTS `{$prefix}analytics_podcasts`"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,20 +20,20 @@ class AddAnalyticsWebsiteStoredProcedure extends Migration
|
|||||||
// Example: CALL analytics_website(1,'FR','Firefox');
|
// Example: CALL analytics_website(1,'FR','Firefox');
|
||||||
$procedureName = $this->db->prefixTable('analytics_website');
|
$procedureName = $this->db->prefixTable('analytics_website');
|
||||||
$createQuery = <<<EOD
|
$createQuery = <<<EOD
|
||||||
CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_referer` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA
|
CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_entry_page` VARCHAR(512) CHARSET utf8mb4, IN `p_referer` VARCHAR(512) CHARSET utf8mb4, IN `p_domain` VARCHAR(128) CHARSET utf8mb4, IN `p_keywords` VARCHAR(384) CHARSET utf8mb4) MODIFIES SQL DATA
|
||||||
DETERMINISTIC
|
DETERMINISTIC
|
||||||
SQL SECURITY INVOKER
|
SQL SECURITY INVOKER
|
||||||
COMMENT 'Add one hit in website logs tables.'
|
COMMENT 'Add one hit in website logs tables.'
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO {$procedureName}_by_country(`podcast_id`, `country_code`, `date`)
|
|
||||||
VALUES (p_podcast_id, p_country_code, DATE(NOW()))
|
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
|
||||||
INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`)
|
INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`)
|
||||||
VALUES (p_podcast_id, p_browser, DATE(NOW()))
|
VALUES (p_podcast_id, p_browser, DATE(NOW()))
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `date`)
|
INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `domain`, `keywords`, `date`)
|
||||||
VALUES (p_podcast_id, p_referer, DATE(NOW()))
|
VALUES (p_podcast_id, p_referer, p_domain, p_keywords, DATE(NOW()))
|
||||||
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
|
INSERT INTO {$procedureName}_by_entry_page(`podcast_id`, `entry_page`, `date`)
|
||||||
|
VALUES (p_podcast_id, p_entry_page, DATE(NOW()))
|
||||||
|
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
|
||||||
END
|
END
|
||||||
EOD;
|
EOD;
|
||||||
$this->db->query($createQuery);
|
$this->db->query($createQuery);
|
||||||
|
176
app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FakePodcastsAnalyticsSeeder
|
||||||
|
* Inserts Fake Analytics in the database
|
||||||
|
*
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Database\Seeds;
|
||||||
|
use App\Models\PodcastModel;
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Seeder;
|
||||||
|
|
||||||
|
class FakePodcastsAnalyticsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
$podcast = (new PodcastModel())->first();
|
||||||
|
|
||||||
|
$jsonUserAgents = json_decode(
|
||||||
|
file_get_contents(
|
||||||
|
'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json'
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($podcast) {
|
||||||
|
$firstEpisode = (new EpisodeModel())
|
||||||
|
->selectMin('published_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
for (
|
||||||
|
$date = strtotime($firstEpisode->published_at);
|
||||||
|
$date < strtotime('now');
|
||||||
|
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
|
||||||
|
) {
|
||||||
|
$analytics_podcasts = [];
|
||||||
|
$analytics_podcasts_by_country = [];
|
||||||
|
$analytics_podcasts_by_episode = [];
|
||||||
|
$analytics_podcasts_by_player = [];
|
||||||
|
$analytics_podcasts_by_region = [];
|
||||||
|
|
||||||
|
$episodes = (new EpisodeModel())
|
||||||
|
->where([
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'DATE(published_at) <=' => date('Y-m-d', $date),
|
||||||
|
])
|
||||||
|
->findAll();
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$age = floor(
|
||||||
|
($date - strtotime($episode->published_at)) / 86400
|
||||||
|
);
|
||||||
|
$proba1 = floor(exp(3 - $age / 40)) + 1;
|
||||||
|
|
||||||
|
for (
|
||||||
|
$num_line = 0;
|
||||||
|
$num_line < rand(1, $proba1);
|
||||||
|
$num_line++
|
||||||
|
) {
|
||||||
|
$proba2 = floor(exp(6 - $age / 20)) + 10;
|
||||||
|
|
||||||
|
$player =
|
||||||
|
$jsonUserAgents[
|
||||||
|
rand(1, count($jsonUserAgents) - 1)
|
||||||
|
];
|
||||||
|
$app = isset($player['app']) ? $player['app'] : '';
|
||||||
|
$device = isset($player['device'])
|
||||||
|
? $player['device']
|
||||||
|
: '';
|
||||||
|
$os = isset($player['os']) ? $player['os'] : '';
|
||||||
|
$bot = isset($player['bot']) ? $player['bot'] : 0;
|
||||||
|
|
||||||
|
$fakeIp =
|
||||||
|
rand(0, 255) .
|
||||||
|
'.' .
|
||||||
|
rand(0, 255) .
|
||||||
|
'.' .
|
||||||
|
rand(0, 255) .
|
||||||
|
'.' .
|
||||||
|
rand(0, 255);
|
||||||
|
|
||||||
|
$cityReader = new \GeoIp2\Database\Reader(
|
||||||
|
WRITEPATH .
|
||||||
|
'uploads/GeoLite2-City/GeoLite2-City.mmdb'
|
||||||
|
);
|
||||||
|
|
||||||
|
$countryCode = 'N/A';
|
||||||
|
$regionCode = 'N/A';
|
||||||
|
$latitude = null;
|
||||||
|
$longitude = null;
|
||||||
|
try {
|
||||||
|
$city = $cityReader->city($fakeIp);
|
||||||
|
|
||||||
|
$countryCode = empty($city->country->isoCode)
|
||||||
|
? 'N/A'
|
||||||
|
: $city->country->isoCode;
|
||||||
|
|
||||||
|
$regionCode = empty($city->subdivisions[0]->isoCode)
|
||||||
|
? 'N/A'
|
||||||
|
: $city->subdivisions[0]->isoCode;
|
||||||
|
$latitude = round($city->location->latitude, 3);
|
||||||
|
$longitude = round($city->location->longitude, 3);
|
||||||
|
} catch (\GeoIp2\Exception\AddressNotFoundException $ex) {
|
||||||
|
//Bad luck, bad IP, nothing to do.
|
||||||
|
}
|
||||||
|
|
||||||
|
$hits = rand(0, $proba2);
|
||||||
|
|
||||||
|
$analytics_podcasts[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$analytics_podcasts_by_country[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'country_code' => $countryCode,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$analytics_podcasts_by_episode[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'episode_id' => $episode->id,
|
||||||
|
'age' => $age,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$analytics_podcasts_by_player[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'app' => $app,
|
||||||
|
'device' => $device,
|
||||||
|
'os' => $os,
|
||||||
|
'bot' => $bot,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$analytics_podcasts_by_region[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'country_code' => $countryCode,
|
||||||
|
'region_code' => $regionCode,
|
||||||
|
'latitude' => $latitude,
|
||||||
|
'longitude' => $longitude,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->db
|
||||||
|
->table('analytics_podcasts')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($analytics_podcasts);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_podcasts_by_country')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($analytics_podcasts_by_country);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_podcasts_by_episode')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($analytics_podcasts_by_episode);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_podcasts_by_player')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($analytics_podcasts_by_player);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_podcasts_by_region')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($analytics_podcasts_by_region);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Create one podcast and some episodes first.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
260
app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class FakeWebsiteAnalyticsSeeder
|
||||||
|
* Inserts Fake Analytics in the database
|
||||||
|
*
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Database\Seeds;
|
||||||
|
use App\Models\PodcastModel;
|
||||||
|
use App\Models\EpisodeModel;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Seeder;
|
||||||
|
|
||||||
|
class FakeWebsiteAnalyticsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
protected $keywords = [
|
||||||
|
'all the smoke podcast',
|
||||||
|
'apple podcast',
|
||||||
|
'bad friends podcast',
|
||||||
|
'best podcast',
|
||||||
|
'best podcasts',
|
||||||
|
'best podcasts 2020',
|
||||||
|
'blood ties',
|
||||||
|
'call her daddy',
|
||||||
|
'call her daddy podcast',
|
||||||
|
'call her daddy podcast controversy',
|
||||||
|
'call her daddy podcast drama',
|
||||||
|
'counter clock podcast',
|
||||||
|
'counterclock podcast',
|
||||||
|
'crime junkie podcast',
|
||||||
|
'crime podcast',
|
||||||
|
'down the hill podcast',
|
||||||
|
'gerry callahan podcast',
|
||||||
|
'google podcast',
|
||||||
|
'history podcast',
|
||||||
|
'joe rogan',
|
||||||
|
'joe rogan podcast',
|
||||||
|
'lana rhoades and logan paul podcast',
|
||||||
|
'last podcast on the left',
|
||||||
|
'michael moore podcast',
|
||||||
|
'michelle obama podcast',
|
||||||
|
'missing in alaska podcast',
|
||||||
|
'murder podcast',
|
||||||
|
'nice white parents podcast',
|
||||||
|
'nick cannon podcast',
|
||||||
|
'npr podcast',
|
||||||
|
'office ladies podcast',
|
||||||
|
'podcast app',
|
||||||
|
'podcasts',
|
||||||
|
'rogan podcast',
|
||||||
|
'rudy giuliani podcast',
|
||||||
|
'savage podcast',
|
||||||
|
'serial podcast',
|
||||||
|
'smartless podcast',
|
||||||
|
'ted cruz podcast',
|
||||||
|
'the daily',
|
||||||
|
'the daily podcast',
|
||||||
|
'the last podcast on the left',
|
||||||
|
'the new abnormal podcast',
|
||||||
|
'tiger king podcast',
|
||||||
|
'trey gowdy podcast',
|
||||||
|
'true crime podcast',
|
||||||
|
'what is a podcast',
|
||||||
|
'what is podcast',
|
||||||
|
'wind of change podcast',
|
||||||
|
'your own backyard podcast',
|
||||||
|
];
|
||||||
|
protected $domains = [
|
||||||
|
'360.cn ',
|
||||||
|
'adobe.com ',
|
||||||
|
'aliexpress.com ',
|
||||||
|
'alipay.com ',
|
||||||
|
'amazon.co.jp ',
|
||||||
|
'amazon.com ',
|
||||||
|
'amazon.in ',
|
||||||
|
'apple.com ',
|
||||||
|
'baidu.com ',
|
||||||
|
'bing.com ',
|
||||||
|
'bongacams.com ',
|
||||||
|
'chaturbate.com ',
|
||||||
|
'china.com.cn ',
|
||||||
|
'csdn.net ',
|
||||||
|
'ebay.com ',
|
||||||
|
'facebook.com ',
|
||||||
|
'google.co.in ',
|
||||||
|
'google.com ',
|
||||||
|
'google.com.hk ',
|
||||||
|
'instagram.com ',
|
||||||
|
'jd.com ',
|
||||||
|
'live.com ',
|
||||||
|
'livejasmin.com ',
|
||||||
|
'microsoft.com ',
|
||||||
|
'microsoftonline.com ',
|
||||||
|
'myshopify.com ',
|
||||||
|
'naver.com ',
|
||||||
|
'netflix.com ',
|
||||||
|
'office.com ',
|
||||||
|
'okezone.com ',
|
||||||
|
'panda.tv ',
|
||||||
|
'qq.com ',
|
||||||
|
'reddit.com ',
|
||||||
|
'sina.com.cn ',
|
||||||
|
'sohu.com ',
|
||||||
|
'taobao.com ',
|
||||||
|
'tianya.cn ',
|
||||||
|
'tmall.com ',
|
||||||
|
'tribunnews.com ',
|
||||||
|
'twitch.tv ',
|
||||||
|
'twitter.com ',
|
||||||
|
'vk.com ',
|
||||||
|
'weibo.com ',
|
||||||
|
'wikipedia.org ',
|
||||||
|
'xinhuanet.com ',
|
||||||
|
'yahoo.co.jp ',
|
||||||
|
'yahoo.com ',
|
||||||
|
'youtube.com ',
|
||||||
|
'zhanqi.tv ',
|
||||||
|
'zoom.us ',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $browsers = [
|
||||||
|
'Android Browser',
|
||||||
|
'Avast Secure Browser',
|
||||||
|
'BlackBerry Browser',
|
||||||
|
'Chrome',
|
||||||
|
'Chrome Mobile',
|
||||||
|
'Chrome Mobile iOS',
|
||||||
|
'Chrome Webview',
|
||||||
|
'Chromium',
|
||||||
|
'Ecosia',
|
||||||
|
'Fennec',
|
||||||
|
'Firebird',
|
||||||
|
'Firefox',
|
||||||
|
'Firefox Mobile',
|
||||||
|
'Firefox Mobile iOS',
|
||||||
|
'Galeon',
|
||||||
|
'GNOME Web',
|
||||||
|
'Headless Chrome',
|
||||||
|
'Huawei Browser',
|
||||||
|
'IE Mobile',
|
||||||
|
'Inconnu',
|
||||||
|
'Internet Explorer',
|
||||||
|
'Kindle Browser',
|
||||||
|
'Konqueror',
|
||||||
|
'Maxthon',
|
||||||
|
'Meizu Browser',
|
||||||
|
'Microsoft Edge',
|
||||||
|
'MIUI Browser',
|
||||||
|
'Mobile Safari',
|
||||||
|
'Mobile Silk',
|
||||||
|
'OmniWeb',
|
||||||
|
'Openwave Mobile Browser',
|
||||||
|
'Opera',
|
||||||
|
'Opera Mini',
|
||||||
|
'Opera Mobile',
|
||||||
|
'Opera Next',
|
||||||
|
'Palm Blazer',
|
||||||
|
'Puffin',
|
||||||
|
'QupZilla',
|
||||||
|
'Safari',
|
||||||
|
'Samsung Browser',
|
||||||
|
'UC Browser',
|
||||||
|
'WOSBrowser',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
$podcast = (new PodcastModel())->first();
|
||||||
|
|
||||||
|
if ($podcast) {
|
||||||
|
$firstEpisode = (new EpisodeModel())
|
||||||
|
->selectMin('published_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
for (
|
||||||
|
$date = strtotime($firstEpisode->published_at);
|
||||||
|
$date < strtotime('now');
|
||||||
|
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
|
||||||
|
) {
|
||||||
|
$website_by_browser = [];
|
||||||
|
$website_by_entry_page = [];
|
||||||
|
$website_by_referer = [];
|
||||||
|
|
||||||
|
$episodes = (new EpisodeModel())
|
||||||
|
->where([
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'DATE(published_at) <=' => date('Y-m-d', $date),
|
||||||
|
])
|
||||||
|
->findAll();
|
||||||
|
foreach ($episodes as $episode) {
|
||||||
|
$age = floor(
|
||||||
|
($date - strtotime($episode->published_at)) / 86400
|
||||||
|
);
|
||||||
|
$proba1 = floor(exp(3 - $age / 40)) + 1;
|
||||||
|
|
||||||
|
for (
|
||||||
|
$num_line = 0;
|
||||||
|
$num_line < rand(1, $proba1);
|
||||||
|
$num_line++
|
||||||
|
) {
|
||||||
|
$proba2 = floor(exp(6 - $age / 20)) + 10;
|
||||||
|
|
||||||
|
$domain =
|
||||||
|
$this->domains[rand(0, count($this->domains) - 1)];
|
||||||
|
$keyword =
|
||||||
|
$this->keywords[
|
||||||
|
rand(0, count($this->keywords) - 1)
|
||||||
|
];
|
||||||
|
$browser =
|
||||||
|
$this->browsers[
|
||||||
|
rand(0, count($this->browsers) - 1)
|
||||||
|
];
|
||||||
|
|
||||||
|
$hits = rand(0, $proba2);
|
||||||
|
|
||||||
|
$website_by_browser[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'browser' => $browser,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$website_by_entry_page[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'entry_page' => $episode->link,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
$website_by_referer[] = [
|
||||||
|
'podcast_id' => $podcast->id,
|
||||||
|
'date' => date('Y-m-d', $date),
|
||||||
|
'referer' =>
|
||||||
|
'http://' . $domain . '/?q=' . $keyword,
|
||||||
|
'domain' => $domain,
|
||||||
|
'keywords' => $keyword,
|
||||||
|
'hits' => $hits,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->db
|
||||||
|
->table('analytics_website_by_browser')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($website_by_browser);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_website_by_entry_page')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($website_by_entry_page);
|
||||||
|
$this->db
|
||||||
|
->table('analytics_website_by_referer')
|
||||||
|
->ignore(true)
|
||||||
|
->insertBatch($website_by_referer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Create one podcast and some episodes first.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AnalyticsWebsiteByCountry
|
* Class AnalyticsPodcasts
|
||||||
* Entity for AnalyticsWebsiteByCountry
|
* Entity for AnalyticsPodcasts
|
||||||
* @copyright 2020 Podlibre
|
* @copyright 2020 Podlibre
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
@ -12,11 +12,10 @@ namespace App\Entities;
|
|||||||
|
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class AnalyticsWebsiteByCountry extends Entity
|
class AnalyticsPodcasts extends Entity
|
||||||
{
|
{
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'podcast_id' => 'integer',
|
'podcast_id' => 'integer',
|
||||||
'country_code' => 'string',
|
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
'hits' => 'integer',
|
'hits' => 'integer',
|
||||||
];
|
];
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AnalyticsEpisodesByPlayer
|
* Class AnalyticsPodcastsByEpisode
|
||||||
* Entity for AnalyticsEpisodesByPlayer
|
* Entity for AnalyticsPodcastsByEpisode
|
||||||
* @copyright 2020 Podlibre
|
* @copyright 2020 Podlibre
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
@ -12,12 +12,11 @@ namespace App\Entities;
|
|||||||
|
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class AnalyticsEpisodesByPlayer extends Entity
|
class AnalyticsPodcastsByEpisode extends Entity
|
||||||
{
|
{
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'podcast_id' => 'integer',
|
'podcast_id' => 'integer',
|
||||||
'episode_id' => 'integer',
|
'episode_id' => 'integer',
|
||||||
'player' => 'string',
|
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
'hits' => 'integer',
|
'hits' => 'integer',
|
||||||
];
|
];
|
@ -16,7 +16,10 @@ class AnalyticsPodcastsByPlayer extends Entity
|
|||||||
{
|
{
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'podcast_id' => 'integer',
|
'podcast_id' => 'integer',
|
||||||
'player' => 'string',
|
'app' => '?string',
|
||||||
|
'device' => '?string',
|
||||||
|
'os' => '?string',
|
||||||
|
'bot' => 'boolean',
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
'hits' => 'integer',
|
'hits' => 'integer',
|
||||||
];
|
];
|
||||||
|
26
app/Entities/AnalyticsPodcastsByRegion.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AnalyticsPodcastsByRegion
|
||||||
|
* Entity for AnalyticsPodcastsByRegion
|
||||||
|
* @copyright 2020 Podlibre
|
||||||
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
|
* @link https://castopod.org/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Entities;
|
||||||
|
|
||||||
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
|
class AnalyticsPodcastsByRegion extends Entity
|
||||||
|
{
|
||||||
|
protected $casts = [
|
||||||
|
'podcast_id' => 'integer',
|
||||||
|
'country_code' => 'string',
|
||||||
|
'region_code' => '?string',
|
||||||
|
'latitude' => '?float',
|
||||||
|
'longitude' => '?float',
|
||||||
|
'date' => 'datetime',
|
||||||
|
'hits' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AnalyticsEpisodesByCountry
|
* Class AnalyticsWebsiteByEntryPage
|
||||||
* Entity for AnalyticsEpisodesByCountry
|
* Entity for AnalyticsWebsiteByEntryPage
|
||||||
* @copyright 2020 Podlibre
|
* @copyright 2020 Podlibre
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
@ -12,12 +12,11 @@ namespace App\Entities;
|
|||||||
|
|
||||||
use CodeIgniter\Entity;
|
use CodeIgniter\Entity;
|
||||||
|
|
||||||
class AnalyticsEpisodesByCountry extends Entity
|
class AnalyticsWebsiteByEntryPage extends Entity
|
||||||
{
|
{
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'podcast_id' => 'integer',
|
'podcast_id' => 'integer',
|
||||||
'episode_id' => 'integer',
|
'entry_page' => '?string',
|
||||||
'country_code' => 'string',
|
|
||||||
'date' => 'datetime',
|
'date' => 'datetime',
|
||||||
'hits' => 'integer',
|
'hits' => 'integer',
|
||||||
];
|
];
|
@ -64,6 +64,7 @@ class Episode extends Entity
|
|||||||
'enclosure_duration' => 'integer',
|
'enclosure_duration' => 'integer',
|
||||||
'enclosure_mimetype' => 'string',
|
'enclosure_mimetype' => 'string',
|
||||||
'enclosure_filesize' => 'integer',
|
'enclosure_filesize' => 'integer',
|
||||||
|
'enclosure_headersize' => 'integer',
|
||||||
'description' => 'string',
|
'description' => 'string',
|
||||||
'image_uri' => '?string',
|
'image_uri' => '?string',
|
||||||
'parental_advisory' => '?string',
|
'parental_advisory' => '?string',
|
||||||
@ -143,6 +144,8 @@ class Episode extends Entity
|
|||||||
$enclosure_metadata['mime_type'];
|
$enclosure_metadata['mime_type'];
|
||||||
$this->attributes['enclosure_filesize'] =
|
$this->attributes['enclosure_filesize'] =
|
||||||
$enclosure_metadata['filesize'];
|
$enclosure_metadata['filesize'];
|
||||||
|
$this->attributes['enclosure_headersize'] =
|
||||||
|
$enclosure_metadata['avdataoffset'];
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -167,6 +170,19 @@ class Episode extends Entity
|
|||||||
'analytics_hit',
|
'analytics_hit',
|
||||||
$this->attributes['podcast_id'],
|
$this->attributes['podcast_id'],
|
||||||
$this->attributes['id'],
|
$this->attributes['id'],
|
||||||
|
// bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
|
||||||
|
// - if file is shorter than 60sec, then it's enclosure_filesize
|
||||||
|
// - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds
|
||||||
|
$this->attributes['enclosure_duration'] <= 60
|
||||||
|
? $this->attributes['enclosure_filesize']
|
||||||
|
: $this->attributes['enclosure_headersize'] +
|
||||||
|
floor(
|
||||||
|
(($this->attributes['enclosure_filesize'] -
|
||||||
|
$this->attributes['enclosure_headersize']) /
|
||||||
|
$this->attributes['enclosure_duration']) *
|
||||||
|
60
|
||||||
|
),
|
||||||
|
$this->attributes['enclosure_filesize'],
|
||||||
$this->attributes['enclosure_uri']
|
$this->attributes['enclosure_uri']
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -82,6 +82,7 @@ class Podcast extends Entity
|
|||||||
'created_by' => 'integer',
|
'created_by' => 'integer',
|
||||||
'updated_by' => 'integer',
|
'updated_by' => 'integer',
|
||||||
'imported_feed_url' => '?string',
|
'imported_feed_url' => '?string',
|
||||||
|
'new_feed_url' => '?string',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,25 +33,56 @@ if (!function_exists('getallheaders')) {
|
|||||||
/**
|
/**
|
||||||
* Set user country in session variable, for analytics purpose
|
* Set user country in session variable, for analytics purpose
|
||||||
*/
|
*/
|
||||||
function set_user_session_country()
|
function set_user_session_deny_list_ip()
|
||||||
{
|
{
|
||||||
$session = \Config\Services::session();
|
$session = \Config\Services::session();
|
||||||
$session->start();
|
$session->start();
|
||||||
|
|
||||||
$country = 'N/A';
|
if (!$session->has('denyListIp')) {
|
||||||
|
$session->set(
|
||||||
|
'denyListIp',
|
||||||
|
\Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finds country:
|
/**
|
||||||
if (!$session->has('country')) {
|
* Set user country in session variable, for analytics purpose
|
||||||
|
*/
|
||||||
|
function set_user_session_location()
|
||||||
|
{
|
||||||
|
$session = \Config\Services::session();
|
||||||
|
$session->start();
|
||||||
|
|
||||||
|
$location = [
|
||||||
|
'countryCode' => 'N/A',
|
||||||
|
'regionCode' => 'N/A',
|
||||||
|
'latitude' => null,
|
||||||
|
'longitude' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Finds location:
|
||||||
|
if (!$session->has('location')) {
|
||||||
try {
|
try {
|
||||||
$reader = new \GeoIp2\Database\Reader(
|
$cityReader = new \GeoIp2\Database\Reader(
|
||||||
WRITEPATH . 'uploads/GeoLite2-Country/GeoLite2-Country.mmdb'
|
WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'
|
||||||
);
|
);
|
||||||
$geoip = $reader->country($_SERVER['REMOTE_ADDR']);
|
$city = $cityReader->city($_SERVER['REMOTE_ADDR']);
|
||||||
$country = $geoip->country->isoCode;
|
|
||||||
|
$location = [
|
||||||
|
'countryCode' => empty($city->country->isoCode)
|
||||||
|
? 'N/A'
|
||||||
|
: $city->country->isoCode,
|
||||||
|
'regionCode' => empty($city->subdivisions[0]->isoCode)
|
||||||
|
? 'N/A'
|
||||||
|
: $city->subdivisions[0]->isoCode,
|
||||||
|
'latitude' => round($city->location->latitude, 3),
|
||||||
|
'longitude' => round($city->location->longitude, 3),
|
||||||
|
];
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// If things go wrong the show must go on and the user must be able to download the file
|
// If things go wrong the show must go on and the user must be able to download the file
|
||||||
}
|
}
|
||||||
$session->set('country', $country);
|
$session->set('location', $location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,58 +98,36 @@ function set_user_session_player()
|
|||||||
$session = \Config\Services::session();
|
$session = \Config\Services::session();
|
||||||
$session->start();
|
$session->start();
|
||||||
|
|
||||||
$playerName = '- Unknown Player -';
|
$playerFound = null;
|
||||||
|
$userAgent = $_SERVER['HTTP_USER_AGENT'];
|
||||||
$useragent = $_SERVER['HTTP_USER_AGENT'];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$jsonUserAgents = json_decode(
|
$playerFound = \Podlibre\UserAgentsPhp\UserAgents::find($userAgent);
|
||||||
file_get_contents(
|
|
||||||
WRITEPATH . 'uploads/user-agents/src/user-agents.json'
|
|
||||||
),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
//Search for current HTTP_USER_AGENT in json file:
|
|
||||||
foreach ($jsonUserAgents as $player) {
|
|
||||||
foreach ($player['user_agents'] as $useragentsRegexp) {
|
|
||||||
//Does the HTTP_USER_AGENT match this regexp:
|
|
||||||
if (preg_match("#{$useragentsRegexp}#", $useragent)) {
|
|
||||||
if (isset($player['bot'])) {
|
|
||||||
//It’s a bot!
|
|
||||||
$playerName = '- Bot -';
|
|
||||||
} else {
|
|
||||||
//It isn’t a bot, we store device/os/app:
|
|
||||||
$playerName =
|
|
||||||
(isset($player['device'])
|
|
||||||
? $player['device'] . '/'
|
|
||||||
: '') .
|
|
||||||
(isset($player['os'])
|
|
||||||
? $player['os'] . '/'
|
|
||||||
: '') .
|
|
||||||
(isset($player['app']) ? $player['app'] : '?');
|
|
||||||
}
|
|
||||||
//We found it!
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// If things go wrong the show must go on and the user must be able to download the file
|
// If things go wrong the show must go on and the user must be able to download the file
|
||||||
}
|
}
|
||||||
if ($playerName == '- Unknown Player -') {
|
if ($playerFound) {
|
||||||
|
$session->set('player', $playerFound);
|
||||||
|
} else {
|
||||||
|
$session->set('player', [
|
||||||
|
'app' => '- unknown -',
|
||||||
|
'device' => '',
|
||||||
|
'os' => '',
|
||||||
|
'bot' => 0,
|
||||||
|
]);
|
||||||
// Add to unknown list
|
// Add to unknown list
|
||||||
try {
|
try {
|
||||||
$db = \Config\Database::connect();
|
$db = \Config\Database::connect();
|
||||||
$procedureNameAUU = $db->prefixTable(
|
$procedureNameAnalyticsUnknownUseragents = $db->prefixTable(
|
||||||
'analytics_unknown_useragents'
|
'analytics_unknown_useragents'
|
||||||
);
|
);
|
||||||
$db->query("CALL $procedureNameAUU(?)", [$useragent]);
|
$db->query("CALL $procedureNameAnalyticsUnknownUseragents(?)", [
|
||||||
|
$userAgent,
|
||||||
|
]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// If things go wrong the show must go on and the user must be able to download the file
|
// If things go wrong the show must go on and the user must be able to download the file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$session->set('player', $playerName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,49 +174,149 @@ function set_user_session_referer()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user entry page in session variable, for analytics purpose
|
||||||
|
*/
|
||||||
|
function set_user_session_entry_page()
|
||||||
|
{
|
||||||
|
$session = \Config\Services::session();
|
||||||
|
$session->start();
|
||||||
|
|
||||||
|
$entryPage = $_SERVER['REQUEST_URI'];
|
||||||
|
if (!$session->has('entryPage')) {
|
||||||
|
$session->set('entryPage', $entryPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function webpage_hit($podcast_id)
|
function webpage_hit($podcast_id)
|
||||||
{
|
{
|
||||||
$session = \Config\Services::session();
|
$session = \Config\Services::session();
|
||||||
$session->start();
|
$session->start();
|
||||||
$db = \Config\Database::connect();
|
|
||||||
|
|
||||||
$procedureName = $db->prefixTable('analytics_website');
|
if (!$session->get('denyListIp')) {
|
||||||
$db->query("call $procedureName(?,?,?,?)", [
|
$db = \Config\Database::connect();
|
||||||
$podcast_id,
|
|
||||||
$session->get('country'),
|
$referer = $session->get('referer');
|
||||||
$session->get('browser'),
|
$domain = empty(parse_url($referer, PHP_URL_HOST))
|
||||||
$session->get('referer'),
|
? null
|
||||||
]);
|
: parse_url($referer, PHP_URL_HOST);
|
||||||
|
parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
|
||||||
|
$keywords = empty($queries['q']) ? null : $queries['q'];
|
||||||
|
|
||||||
|
$procedureName = $db->prefixTable('analytics_website');
|
||||||
|
$db->query("call $procedureName(?,?,?,?,?,?)", [
|
||||||
|
$podcast_id,
|
||||||
|
$session->get('browser'),
|
||||||
|
$session->get('entryPage'),
|
||||||
|
$referer,
|
||||||
|
$domain,
|
||||||
|
$keywords,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function podcast_hit($p_podcast_id, $p_episode_id)
|
/**
|
||||||
|
* Counting podcast episode downloads for analytics purposes
|
||||||
|
* ✅ No IP address is ever stored on the server.
|
||||||
|
* ✅ Only aggregate data is stored in the database.
|
||||||
|
* We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
|
||||||
|
* https://iabtechlab.com/standards/podcast-measurement-guidelines/
|
||||||
|
* https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf
|
||||||
|
* ✅ Rolling 24-hour window
|
||||||
|
* ✅ Castopod does not do pre-load
|
||||||
|
* ✅ IP deny list https://github.com/client9/ipcat
|
||||||
|
* ✅ User-agent Filtering https://github.com/opawg/user-agents
|
||||||
|
* ✅ Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app)
|
||||||
|
* ✅ In case of partial content, adds up all requests to check >1mn was downloaded
|
||||||
|
* ✅ Identifying Uniques is done with a combination of IP Address and User Agent
|
||||||
|
* @param int $podcastId The podcast ID
|
||||||
|
* @param int $episodeId The Episode ID
|
||||||
|
* @param int $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn)
|
||||||
|
* @param int $fileSize The podcast complete file size
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
|
||||||
{
|
{
|
||||||
$session = \Config\Services::session();
|
$session = \Config\Services::session();
|
||||||
$session->start();
|
$session->start();
|
||||||
$first_time_for_this_episode = true;
|
|
||||||
|
|
||||||
if ($session->has('episodes')) {
|
// We try to count (but if things went wrong the show should go on and the user should be able to download the file):
|
||||||
if (in_array($p_episode_id, $session->get('episodes'))) {
|
try {
|
||||||
$first_time_for_this_episode = false;
|
// If the user IP is denied it's probably a bot:
|
||||||
|
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 =
|
||||||
|
'_IpUaEp_' .
|
||||||
|
sha1(
|
||||||
|
$_SERVER['REMOTE_ADDR'] .
|
||||||
|
'_' .
|
||||||
|
$_SERVER['HTTP_USER_AGENT'] .
|
||||||
|
'_' .
|
||||||
|
$episodeId
|
||||||
|
);
|
||||||
|
// Was this episode downloaded in the past 24h:
|
||||||
|
$downloadedBytes = cache($hashID);
|
||||||
|
// Rolling window is 24 hours (86400 seconds):
|
||||||
|
$ttl = 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();
|
||||||
} else {
|
} else {
|
||||||
$session->push('episodes', [$p_episode_id]);
|
// If it was never downloaded that means that zero byte were downloaded:
|
||||||
|
$downloadedBytes = 0;
|
||||||
}
|
}
|
||||||
} else {
|
// If the number of downloaded bytes was previously below the 1mn threshold we go on:
|
||||||
$session->set('episodes', [$p_episode_id]);
|
// (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)) {
|
||||||
|
$downloadedBytes = $fileSize;
|
||||||
|
} else {
|
||||||
|
// [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working.
|
||||||
|
// We don't count these requests:
|
||||||
|
if ($httpRange != 'bytes=0-1') {
|
||||||
|
// We calculate how many bytes are being downloaded based on HTTP_RANGE values:
|
||||||
|
$ranges = explode(',', substr($httpRange, 6));
|
||||||
|
foreach ($ranges as $range) {
|
||||||
|
$parts = explode('-', $range);
|
||||||
|
$downloadedBytes += empty($parts[1])
|
||||||
|
? $fileSize
|
||||||
|
: $parts[1] - (empty($parts[0]) ? 0 : $parts[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We save the number of downloaded bytes for this user and this episode:
|
||||||
|
cache()->save($hashID, $downloadedBytes, $ttl);
|
||||||
|
|
||||||
if ($first_time_for_this_episode) {
|
// If more that 1mn was downloaded, we send that to the database:
|
||||||
$db = \Config\Database::connect();
|
if ($downloadedBytes >= $bytesThreshold) {
|
||||||
$procedureName = $db->prefixTable('analytics_podcasts');
|
$db = \Config\Database::connect();
|
||||||
try {
|
$procedureName = $db->prefixTable('analytics_podcasts');
|
||||||
$db->query("CALL $procedureName(?,?,?,?);", [
|
|
||||||
$p_podcast_id,
|
$app = $session->get('player')['app'];
|
||||||
$p_episode_id,
|
$device = $session->get('player')['device'];
|
||||||
$session->get('country'),
|
$os = $session->get('player')['os'];
|
||||||
$session->get('player'),
|
$bot = $session->get('player')['bot'];
|
||||||
]);
|
|
||||||
} catch (\Exception $e) {
|
$db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [
|
||||||
// If things go wrong the show must go on and the user must be able to download the file
|
$podcastId,
|
||||||
|
$episodeId,
|
||||||
|
$session->get('location')['countryCode'],
|
||||||
|
$session->get('location')['regionCode'],
|
||||||
|
$session->get('location')['latitude'],
|
||||||
|
$session->get('location')['longitude'],
|
||||||
|
$app == null ? '' : $app,
|
||||||
|
$device == null ? '' : $device,
|
||||||
|
$os == null ? '' : $os,
|
||||||
|
$bot == null ? 0 : $bot,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// If things go wrong the show must go on and the user must be able to download the file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ function get_file_tags($file)
|
|||||||
return [
|
return [
|
||||||
'filesize' => $FileInfo['filesize'],
|
'filesize' => $FileInfo['filesize'],
|
||||||
'mime_type' => $FileInfo['mime_type'],
|
'mime_type' => $FileInfo['mime_type'],
|
||||||
|
'avdataoffset' => $FileInfo['avdataoffset'],
|
||||||
'playtime_seconds' => $FileInfo['playtime_seconds'],
|
'playtime_seconds' => $FileInfo['playtime_seconds'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -68,7 +69,11 @@ function write_enclosure_tags($episode)
|
|||||||
'comment' => [$episode->description],
|
'comment' => [$episode->description],
|
||||||
'track_number' => [strval($episode->number)],
|
'track_number' => [strval($episode->number)],
|
||||||
'copyright_message' => [$episode->podcast->copyright],
|
'copyright_message' => [$episode->podcast->copyright],
|
||||||
'publisher' => ['Podlibre'],
|
'publisher' => [
|
||||||
|
empty($episode->podcast->publisher)
|
||||||
|
? $episode->podcast->owner_name
|
||||||
|
: $episode->podcast->publisher,
|
||||||
|
],
|
||||||
'encoded_by' => ['Castopod'],
|
'encoded_by' => ['Castopod'],
|
||||||
|
|
||||||
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
|
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
|
||||||
|
@ -36,6 +36,14 @@ function get_rss_feed($podcast)
|
|||||||
$atom_link->addAttribute('rel', 'self');
|
$atom_link->addAttribute('rel', 'self');
|
||||||
$atom_link->addAttribute('type', 'application/rss+xml');
|
$atom_link->addAttribute('type', 'application/rss+xml');
|
||||||
|
|
||||||
|
if (!empty($podcast->new_feed_url)) {
|
||||||
|
$channel->addChild(
|
||||||
|
'new-feed-url',
|
||||||
|
$podcast->new_feed_url,
|
||||||
|
$itunes_namespace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// the last build date corresponds to the creation of the feed.xml cache
|
// the last build date corresponds to the creation of the feed.xml cache
|
||||||
$channel->addChild(
|
$channel->addChild(
|
||||||
'lastBuildDate',
|
'lastBuildDate',
|
||||||
@ -50,7 +58,7 @@ function get_rss_feed($podcast)
|
|||||||
$channel->addChild('title', $podcast->title);
|
$channel->addChild('title', $podcast->title);
|
||||||
$channel->addChildWithCDATA('description', $podcast->description_html);
|
$channel->addChildWithCDATA('description', $podcast->description_html);
|
||||||
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
|
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
|
||||||
$itunes_image->addAttribute('href', $podcast->image->url);
|
$itunes_image->addAttribute('href', $podcast->image->original_url);
|
||||||
$channel->addChild('language', $podcast->language);
|
$channel->addChild('language', $podcast->language);
|
||||||
|
|
||||||
// set main category first, then other categories as apple
|
// set main category first, then other categories as apple
|
||||||
|
@ -22,4 +22,5 @@ return [
|
|||||||
'import' => 'feed import',
|
'import' => 'feed import',
|
||||||
'settings' => 'settings',
|
'settings' => 'settings',
|
||||||
'platforms' => 'platforms',
|
'platforms' => 'platforms',
|
||||||
|
'analytics' => 'Analytics',
|
||||||
];
|
];
|
||||||
|
@ -12,7 +12,7 @@ return [
|
|||||||
'create' => 'Create a podcast',
|
'create' => 'Create a podcast',
|
||||||
'import' => 'Import a podcast',
|
'import' => 'Import a podcast',
|
||||||
'new_episode' => 'New Episode',
|
'new_episode' => 'New Episode',
|
||||||
'feed' => 'RSS feed',
|
'feed' => 'RSS',
|
||||||
'view' => 'View podcast',
|
'view' => 'View podcast',
|
||||||
'edit' => 'Edit podcast',
|
'edit' => 'Edit podcast',
|
||||||
'delete' => 'Delete podcast',
|
'delete' => 'Delete podcast',
|
||||||
|
@ -7,6 +7,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'legal_dislaimer_title' => 'Legal Disclaimer',
|
||||||
|
'legal_dislaimer_content' =>
|
||||||
|
'Make sure you own the rights for this podcast before importing it.<br/>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.<br/>The current version does not show any progress while it runs. You will not see anything updated until it is done.<br/>In case of timeout error, increase max_execution_time value.',
|
||||||
'old_podcast_section_title' => 'The podcast to import',
|
'old_podcast_section_title' => 'The podcast to import',
|
||||||
'old_podcast_section_subtitle' => '',
|
'old_podcast_section_subtitle' => '',
|
||||||
'imported_feed_url' => 'Feed URL',
|
'imported_feed_url' => 'Feed URL',
|
||||||
|
@ -20,4 +20,5 @@ return [
|
|||||||
'contributor-add' => 'Add contributor',
|
'contributor-add' => 'Add contributor',
|
||||||
'settings' => 'Settings',
|
'settings' => 'Settings',
|
||||||
'platforms' => 'Podcast platforms',
|
'platforms' => 'Podcast platforms',
|
||||||
|
'podcast-analytics' => 'Audiences Overview',
|
||||||
];
|
];
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AnalyticsPodcastsByCountryModel
|
* Class AnalyticsPodcastsByCountryModel
|
||||||
* Model for analytics_episodes_by_country table in database
|
* Model for analytics_podcasts_by_country table in database
|
||||||
* @copyright 2020 Podlibre
|
* @copyright 2020 Podlibre
|
||||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||||
* @link https://castopod.org/
|
* @link https://castopod.org/
|
||||||
@ -14,8 +14,7 @@ use CodeIgniter\Model;
|
|||||||
|
|
||||||
class AnalyticsPodcastsByCountryModel extends Model
|
class AnalyticsPodcastsByCountryModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'analytics_episodes_by_country';
|
protected $table = 'analytics_podcasts_by_country';
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
protected $allowedFields = [];
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
113
app/Models/AnalyticsPodcastsByEpisodeModel.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AnalyticsPodcastsByEpisodeModel
|
||||||
|
* Model for analytics_podcasts_by_episodes 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 AnalyticsPodcastsByEpisodeModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'analytics_podcasts_by_episode';
|
||||||
|
|
||||||
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
protected $returnType = \App\Entities\AnalyticsPodcastsByEpisode::class;
|
||||||
|
protected $useSoftDeletes = false;
|
||||||
|
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $podcastId, $episodeId
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getDataByDay(int $podcastId, int $episodeId = null): array
|
||||||
|
{
|
||||||
|
if (!$episodeId) {
|
||||||
|
if (
|
||||||
|
!($found = cache(
|
||||||
|
"{$podcastId}_analytics_podcast_by_episode_by_day"
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
$lastEpisodes = (new EpisodeModel())
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,6 @@ use CodeIgniter\Model;
|
|||||||
class AnalyticsPodcastsByPlayerModel extends Model
|
class AnalyticsPodcastsByPlayerModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'analytics_podcasts_by_player';
|
protected $table = 'analytics_podcasts_by_player';
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
protected $allowedFields = [];
|
protected $allowedFields = [];
|
||||||
|
|
||||||
@ -23,4 +22,120 @@ class AnalyticsPodcastsByPlayerModel extends Model
|
|||||||
protected $useSoftDeletes = false;
|
protected $useSoftDeletes = false;
|
||||||
|
|
||||||
protected $useTimestamps = 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
25
app/Models/AnalyticsPodcastsByRegionModel.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
55
app/Models/AnalyticsPodcastsModel.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AnalyticsPodcastsModel
|
||||||
|
* 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 AnalyticsPodcastsModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'analytics_podcasts';
|
||||||
|
|
||||||
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
protected $returnType = \App\Entities\AnalyticsPodcasts::class;
|
||||||
|
protected $useSoftDeletes = false;
|
||||||
|
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all 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`')
|
||||||
|
->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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,6 @@ use CodeIgniter\Model;
|
|||||||
class AnalyticsWebsiteByBrowserModel extends Model
|
class AnalyticsWebsiteByBrowserModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'analytics_website_by_browser';
|
protected $table = 'analytics_website_by_browser';
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
protected $allowedFields = [];
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
25
app/Models/AnalyticsWebsiteByEntryPageModel.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AnalyticsWebsiteByEntryPageModel
|
||||||
|
* Model for 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/
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use CodeIgniter\Model;
|
||||||
|
|
||||||
|
class AnalyticsWebsiteByEntryPageModel extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'analytics_website_by_entry_page';
|
||||||
|
|
||||||
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
protected $returnType = \App\Entities\AnalyticsWebsiteByEntryPage::class;
|
||||||
|
protected $useSoftDeletes = false;
|
||||||
|
|
||||||
|
protected $useTimestamps = false;
|
||||||
|
}
|
@ -15,7 +15,6 @@ use CodeIgniter\Model;
|
|||||||
class AnalyticsWebsiteByRefererModel extends Model
|
class AnalyticsWebsiteByRefererModel extends Model
|
||||||
{
|
{
|
||||||
protected $table = 'analytics_website_by_referer';
|
protected $table = 'analytics_website_by_referer';
|
||||||
protected $primaryKey = 'id';
|
|
||||||
|
|
||||||
protected $allowedFields = [];
|
protected $allowedFields = [];
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ class EpisodeModel extends Model
|
|||||||
'enclosure_duration',
|
'enclosure_duration',
|
||||||
'enclosure_mimetype',
|
'enclosure_mimetype',
|
||||||
'enclosure_filesize',
|
'enclosure_filesize',
|
||||||
|
'enclosure_headersize',
|
||||||
'description',
|
'description',
|
||||||
'image_uri',
|
'image_uri',
|
||||||
'parental_advisory',
|
'parental_advisory',
|
||||||
|
@ -35,6 +35,7 @@ class PodcastModel extends Model
|
|||||||
'created_by',
|
'created_by',
|
||||||
'updated_by',
|
'updated_by',
|
||||||
'imported_feed_url',
|
'imported_feed_url',
|
||||||
|
'new_feed_url',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $returnType = \App\Entities\Podcast::class;
|
protected $returnType = \App\Entities\Podcast::class;
|
||||||
|
4
app/Views/_assets/charts.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import "core-js";
|
||||||
|
import DrawCharts from "./modules/Charts";
|
||||||
|
|
||||||
|
DrawCharts();
|
26
app/Views/_assets/images/logo-castopod-circle.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata>
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#009486;}
|
||||||
|
.st1{fill:#E7F9E4;}
|
||||||
|
.st2{fill:none;}
|
||||||
|
.st3{fill:#E7FFE3;}
|
||||||
|
</style>
|
||||||
|
<circle cx="32" cy="32" r="32" fill="#e7f9e4" stroke-width="2.0334"/><g transform="matrix(.24971 0 0 .24971 6.7291 14.595)">
|
||||||
|
<path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/>
|
||||||
|
<path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/>
|
||||||
|
<path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/>
|
||||||
|
<path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/>
|
||||||
|
<path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/>
|
||||||
|
|
||||||
|
|
||||||
|
<path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.5 KiB |
@ -1,86 +1,40 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg
|
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
viewBox="0 0 202.4 137.8" style="enable-background:new 0 0 202.4 137.8;" xml:space="preserve">
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
<style type="text/css">
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
.st0{fill:#009486;}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
.st1{fill:#E7F9E4;}
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
.st2{fill:none;}
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
.st3{fill:#E7FFE3;}
|
||||||
inkscape:version="1.0beta1 (ee59332, 2019-11-28)"
|
</style>
|
||||||
sodipodi:docname="castopod.svg"
|
<g>
|
||||||
id="svg839"
|
<path id="dark_greeen_19_" class="st0" d="M181.9,131.7h-32.5c0,0-1.2-2.5-2.5-4.9c-1.3-2.4-4.4-2.3-4.4-2.3H59.7
|
||||||
version="1.1"
|
c0,0-3-0.4-4.5,2.3c-1.6,2.7-2.6,4.9-2.6,4.9H20.6c-6.9,0-12.6-5.6-12.6-12.5V20.3c0-6.9,5.6-12.6,12.5-12.6h161.3
|
||||||
viewBox="0 0 64 63.999998"
|
c6.9,0,12.6,5.6,12.6,12.5v98.9C194.5,126,188.8,131.7,181.9,131.7z"/>
|
||||||
height="64"
|
<path class="st1" d="M143.7,34.5H58.6c-14.6,0-26.5,12-26.5,26.6c0,14.6,11.9,26.5,26.5,26.5h85.1c14.6,0,26.5-11.9,26.5-26.5
|
||||||
width="64">
|
C170.3,46.3,158.4,34.4,143.7,34.5z M68.3,68.7c0,0-3.9-2.9-9.4-2.9c-4.1,0-8.9,2.5-8.9,2.5c-1.3-1.9-2.1-4.1-2.1-6.6
|
||||||
<metadata
|
c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C70.7,64.4,69.8,66.8,68.3,68.7z M101.2,75.3c-12.5,0-12-9.6-12-9.6
|
||||||
id="metadata845">
|
c-0.2-1.8,2.1-2.4,2.9-1.3c0.4,0.6,0.4,0.6,0.7,1.7c1.7,5.9,8.4,5.6,8.4,5.6s6.7,0.4,8.4-5.6c0.3-1,0.3-1.1,0.7-1.7
|
||||||
<rdf:RDF>
|
c0.8-1,3.1-0.5,2.9,1.3C113.2,65.7,113.7,75.3,101.2,75.3z M152.3,68.4c0,0-4.8-2.5-8.9-2.5c-5.5,0-9.4,2.9-9.4,2.9
|
||||||
<cc:Work
|
c-1.5-1.9-2.4-4.3-2.4-7c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C154.5,64.2,153.7,66.5,152.3,68.4z"/>
|
||||||
rdf:about="">
|
<path class="st2" d="M110.3,64.3c-0.4,0.6-0.4,0.6-0.7,1.7c-1.7,5.9-8.4,5.6-8.4,5.6s-6.7,0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7
|
||||||
<dc:format>image/svg+xml</dc:format>
|
c-0.8-1-3.1-0.5-2.9,1.3c0,0-0.5,9.6,12,9.6c12.5,0,12-9.6,12-9.6C113.4,63.9,111.1,63.3,110.3,64.3z"/>
|
||||||
<dc:type
|
<path class="st2" d="M143.1,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.6,0.9,5,2.4,7c0,0,3.9-2.9,9.4-2.9c4.1,0,8.9,2.5,8.9,2.5
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
c1.3-1.9,2.1-4.1,2.1-6.6C154.5,55.5,149.4,50.4,143.1,50.4z"/>
|
||||||
<dc:title></dc:title>
|
<path class="st2" d="M59.3,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.5,0.8,4.7,2.1,6.6c0,0,4.8-2.5,8.9-2.5c5.5,0,9.4,2.9,9.4,2.9
|
||||||
</cc:Work>
|
c1.5-1.9,2.4-4.3,2.4-7C70.7,55.5,65.6,50.4,59.3,50.4z"/>
|
||||||
</rdf:RDF>
|
<g>
|
||||||
</metadata>
|
<g>
|
||||||
<defs
|
<path class="st3" d="M47.1,23.3c-6.3-1.7-11.7,2.1-14.7,7.3c-0.7,1.2-0.2,2.2,0.5,2.6c1,0.3,1.7,0.1,2.8-1.5
|
||||||
id="defs843" />
|
c2.2-3.9,5.9-6.1,10.1-5.3c0,0,2.9,0.9,3.3-1C49.4,24.2,48.3,23.6,47.1,23.3z"/>
|
||||||
<sodipodi:namedview
|
</g>
|
||||||
inkscape:current-layer="svg839"
|
</g>
|
||||||
inkscape:window-maximized="0"
|
<g>
|
||||||
inkscape:window-y="23"
|
<g>
|
||||||
inkscape:window-x="0"
|
<path class="st3" d="M159.9,27.3c-0.1,1.9,2.9,1.9,2.9,1.9c4.2,0.4,6.8,2.3,7.8,6.7c0.6,1.9,1.2,2.2,2.3,2.2
|
||||||
inkscape:cy="33.560512"
|
c0.8-0.1,1.6-1,1.2-2.4c-1.4-5.8-5.1-9.8-11.7-9.9C161.2,25.7,160,26,159.9,27.3z"/>
|
||||||
inkscape:cx="32"
|
</g>
|
||||||
inkscape:zoom="8.9714173"
|
</g>
|
||||||
showgrid="false"
|
</g>
|
||||||
id="namedview841"
|
|
||||||
inkscape:window-height="1035"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
guidetolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
objecttolerance="10"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
bordercolor="#666666"
|
|
||||||
pagecolor="#ffffff" />
|
|
||||||
<circle
|
|
||||||
id="greencircle"
|
|
||||||
fill="#37c837"
|
|
||||||
cx="32"
|
|
||||||
cy="32"
|
|
||||||
r="31.684" />
|
|
||||||
<g
|
|
||||||
id="speak">
|
|
||||||
<path
|
|
||||||
d="M45.21 20.22H18.79c-6.473 0-11.74 5.266-11.74 11.74S12.317 43.7 18.79 43.7h10.756c1.08 0 1.957-.875 1.957-1.956 0-1.08-.877-1.957-1.957-1.957H18.79c-4.315 0-7.826-3.51-7.826-7.827 0-4.316 3.51-7.828 7.827-7.828h26.42c4.315 0 7.826 3.512 7.826 7.828 0 4.316-3.51 7.827-7.827 7.827H43.34v.002c-5.41.096-9.783 4.527-9.783 9.96 0 1.08.875 1.957 1.956 1.957 1.08 0 1.956-.876 1.956-1.957 0-3.336 2.714-6.05 6.05-6.05h1.687c6.473 0 11.74-5.266 11.74-11.74s-5.267-11.74-11.74-11.74"
|
|
||||||
fill="#fff"
|
|
||||||
id="phylactery" />
|
|
||||||
<g
|
|
||||||
id="threedots">
|
|
||||||
<circle
|
|
||||||
r="2"
|
|
||||||
cy="32"
|
|
||||||
cx="24.256159"
|
|
||||||
id="leftdot"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;" />
|
|
||||||
<circle
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;"
|
|
||||||
id="middledot"
|
|
||||||
cx="32"
|
|
||||||
cy="32"
|
|
||||||
r="2" />
|
|
||||||
<circle
|
|
||||||
r="2"
|
|
||||||
cy="32"
|
|
||||||
cx="39.743839"
|
|
||||||
id="rightdot"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.4 KiB |
@ -1,7 +1,26 @@
|
|||||||
<svg id="default" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<rect width="300" height="300" rx="67" fill="#ebfaeb"/>
|
<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata>
|
||||||
<path id="phylactery" d="M195,112.36H105a40,40,0,0,0,0,80h36.65a6.67,6.67,0,0,0,0-13.33H105a26.67,26.67,0,0,1,0-53.34h90A26.67,26.67,0,0,1,195,179h-6.37A34,34,0,0,0,155.31,213a6.67,6.67,0,1,0,13.33,0,20.64,20.64,0,0,1,20.61-20.61H195a40,40,0,0,0,0-80" fill="#37c837"/>
|
<style type="text/css">
|
||||||
<circle id="leftdot" cx="123.62" cy="152.5" r="6.81" fill="#37c837"/>
|
.st0{fill:#AAAAAA;}
|
||||||
<circle id="middledot" cx="150.01" cy="152.5" r="6.81" fill="#37c837"/>
|
.st1{fill:#CCCCCC;}
|
||||||
<circle id="rightdot" cx="176.39" cy="152.5" r="6.81" fill="#37c837"/>
|
.st2{fill:none;}
|
||||||
|
.st3{fill:#EEEEEE;}
|
||||||
|
</style>
|
||||||
|
<rect width="64" height="64" fill="#fff" stroke-width=".94495"/><g transform="matrix(.32439 0 0 .32439 -.82859 9.3899)">
|
||||||
|
<path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/>
|
||||||
|
<path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/>
|
||||||
|
<path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/>
|
||||||
|
<path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/>
|
||||||
|
<path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/>
|
||||||
|
|
||||||
|
|
||||||
|
<path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 631 B After Width: | Height: | Size: 2.5 KiB |
134
app/Views/_assets/modules/Charts.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
// Import modules
|
||||||
|
import * as am4charts from "@amcharts/amcharts4/charts";
|
||||||
|
import * as am4core from "@amcharts/amcharts4/core";
|
||||||
|
import am4themes_material from "@amcharts/amcharts4/themes/material";
|
||||||
|
|
||||||
|
const drawPieChart = (chartDivId: string, dataUrl: string | null): void => {
|
||||||
|
// Create chart instance
|
||||||
|
const chart = am4core.create(chartDivId, am4charts.PieChart);
|
||||||
|
am4core.percent(100);
|
||||||
|
|
||||||
|
// Set theme
|
||||||
|
am4core.useTheme(am4themes_material);
|
||||||
|
|
||||||
|
chart.innerRadius = am4core.percent(10);
|
||||||
|
|
||||||
|
// Add data
|
||||||
|
chart.dataSource.url = dataUrl || "";
|
||||||
|
chart.dataSource.parser.options.emptyAs = 0;
|
||||||
|
|
||||||
|
// Add and configure Series
|
||||||
|
const pieSeries = chart.series.push(new am4charts.PieSeries());
|
||||||
|
pieSeries.dataFields.value = "values";
|
||||||
|
pieSeries.dataFields.category = "labels";
|
||||||
|
|
||||||
|
pieSeries.slices.template.stroke = am4core.color("#ffffff");
|
||||||
|
pieSeries.slices.template.strokeWidth = 1;
|
||||||
|
pieSeries.slices.template.strokeOpacity = 1;
|
||||||
|
pieSeries.labels.template.disabled = true;
|
||||||
|
pieSeries.ticks.template.disabled = true;
|
||||||
|
|
||||||
|
chart.legend = new am4charts.Legend();
|
||||||
|
chart.legend.position = "right";
|
||||||
|
chart.legend.scrollable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawXYChart = (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;
|
||||||
|
|
||||||
|
chart.yAxes.push(new am4charts.ValueAxis());
|
||||||
|
|
||||||
|
// 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} downloads";
|
||||||
|
|
||||||
|
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
|
||||||
|
): void => {
|
||||||
|
// Create chart instance
|
||||||
|
const chart = am4core.create(chartDivId, am4charts.XYChart);
|
||||||
|
am4core.percent(100);
|
||||||
|
|
||||||
|
// Set theme
|
||||||
|
am4core.useTheme(am4themes_material);
|
||||||
|
|
||||||
|
// Create axes
|
||||||
|
chart.xAxes.push(new am4charts.ValueAxis());
|
||||||
|
chart.yAxes.push(new am4charts.ValueAxis());
|
||||||
|
|
||||||
|
// Add data
|
||||||
|
chart.dataSource.url = dataUrl || "";
|
||||||
|
chart.dataSource.parser.options.emptyAs = 0;
|
||||||
|
|
||||||
|
// Create series
|
||||||
|
const series1 = chart.series.push(new am4charts.LineSeries());
|
||||||
|
series1.dataFields.valueX = "X";
|
||||||
|
series1.dataFields.valueY = "aY";
|
||||||
|
|
||||||
|
const series2 = chart.series.push(new am4charts.LineSeries());
|
||||||
|
series2.dataFields.valueX = "X";
|
||||||
|
series2.dataFields.valueY = "bY";
|
||||||
|
|
||||||
|
const series3 = chart.series.push(new am4charts.LineSeries());
|
||||||
|
series3.dataFields.valueX = "X";
|
||||||
|
series3.dataFields.valueY = "cY";
|
||||||
|
|
||||||
|
const series4 = chart.series.push(new am4charts.LineSeries());
|
||||||
|
series4.dataFields.valueX = "X";
|
||||||
|
series4.dataFields.valueY = "dY";
|
||||||
|
|
||||||
|
const series5 = chart.series.push(new am4charts.LineSeries());
|
||||||
|
series5.dataFields.valueX = "X";
|
||||||
|
series5.dataFields.valueY = "eY";
|
||||||
|
};
|
||||||
|
|
||||||
|
const DrawCharts = (): void => {
|
||||||
|
const chartDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
|
||||||
|
"div[data-chart-type]"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < chartDivs.length; i++) {
|
||||||
|
const chartDiv: HTMLDivElement = chartDivs[i];
|
||||||
|
const chartType = chartDiv.dataset.chartType;
|
||||||
|
switch (chartType) {
|
||||||
|
case "pie-chart":
|
||||||
|
drawPieChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||||
|
break;
|
||||||
|
case "xy-chart":
|
||||||
|
drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||||
|
break;
|
||||||
|
case "xy-series-chart":
|
||||||
|
drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Unknown chart type:" + chartType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DrawCharts;
|
@ -24,7 +24,7 @@
|
|||||||
<?= render_page_links() ?>
|
<?= render_page_links() ?>
|
||||||
<small><?= lang('Common.powered_by', [
|
<small><?= lang('Common.powered_by', [
|
||||||
'castopod' =>
|
'castopod' =>
|
||||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
'<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||||
]) ?></small>
|
]) ?></small>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||||
<link rel="stylesheet" href="/assets/admin.css"/>
|
<link rel="stylesheet" href="/assets/admin.css"/>
|
||||||
<link rel="stylesheet" href="/assets/index.css"/>
|
<link rel="stylesheet" href="/assets/index.css"/>
|
||||||
<script src="/assets/admin.js" defer></script>
|
<script src="/assets/admin.js" type="module" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="relative bg-gray-100 holy-grail-grid">
|
<body class="relative bg-gray-100 holy-grail-grid">
|
||||||
@ -43,12 +43,12 @@
|
|||||||
<footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer">
|
<footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer">
|
||||||
<small><?= lang('Common.powered_by', [
|
<small><?= lang('Common.powered_by', [
|
||||||
'castopod' =>
|
'castopod' =>
|
||||||
'<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
'<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
|
||||||
]) ?></small>
|
]) ?></small>
|
||||||
</footer>
|
</footer>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id="sidebar-toggler"
|
id="sidebar-toggler"
|
||||||
class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline"
|
class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline"
|
||||||
style="transform: translateX(0px);"><?= icon('menu') ?></button>
|
style="transform: translateX(0px);"><?= icon('menu') ?></button>
|
||||||
</body>
|
</body>
|
||||||
|
@ -10,7 +10,7 @@ $podcastNavigation = [
|
|||||||
],
|
],
|
||||||
'analytics' => [
|
'analytics' => [
|
||||||
'icon' => 'line-chart',
|
'icon' => 'line-chart',
|
||||||
'items' => [],
|
'items' => ['podcast-analytics'],
|
||||||
],
|
],
|
||||||
'contributors' => [
|
'contributors' => [
|
||||||
'icon' => 'group',
|
'icon' => 'group',
|
||||||
|
32
app/Views/admin/podcast/analytics.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?= $this->extend('admin/_layout') ?>
|
||||||
|
|
||||||
|
<?= $this->section('title') ?>
|
||||||
|
<?= $podcast->title ?>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('pageTitle') ?>
|
||||||
|
<?= $podcast->title ?>
|
||||||
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
|
<?= $this->section('content') ?>
|
||||||
|
<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
|
||||||
|
'analytics-data',
|
||||||
|
$podcast->id,
|
||||||
|
'PodcastsByPlayer',
|
||||||
|
'ByApp'
|
||||||
|
) ?>"></div>
|
||||||
|
<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
|
||||||
|
'analytics-data',
|
||||||
|
$podcast->id,
|
||||||
|
'Podcasts',
|
||||||
|
'ByDay'
|
||||||
|
) ?>"></div>
|
||||||
|
<div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to(
|
||||||
|
'analytics-data',
|
||||||
|
$podcast->id,
|
||||||
|
'PodcastsByEpisode',
|
||||||
|
'ByDay'
|
||||||
|
) ?>"></div>
|
||||||
|
|
||||||
|
<script src="/assets/charts.js" type="module"></script>
|
||||||
|
<?= $this->endSection() ?>
|
@ -13,7 +13,9 @@
|
|||||||
"codeigniter4/codeigniter4": "dev-develop",
|
"codeigniter4/codeigniter4": "dev-develop",
|
||||||
"league/commonmark": "^1.5",
|
"league/commonmark": "^1.5",
|
||||||
"vlucas/phpdotenv": "^5.2",
|
"vlucas/phpdotenv": "^5.2",
|
||||||
"league/html-to-markdown": "^4.10"
|
"league/html-to-markdown": "^4.10",
|
||||||
|
"podlibre/user-agents-php": "*",
|
||||||
|
"podlibre/ipcat": "*"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mikey179/vfsstream": "1.6.*",
|
"mikey179/vfsstream": "1.6.*",
|
||||||
@ -27,8 +29,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "phpunit",
|
"test": "phpunit",
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php",
|
||||||
|
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php"
|
||||||
|
],
|
||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
"@composer dump-autoload"
|
"@composer dump-autoload",
|
||||||
|
"@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php",
|
||||||
|
"@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
|
146
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "38eeae7f5d0143863430cda9df10d487",
|
"content-hash": "47b9f628f03f8c494a9339b054359ec8",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "codeigniter4/codeigniter4",
|
"name": "codeigniter4/codeigniter4",
|
||||||
@ -12,12 +12,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/codeigniter4/CodeIgniter4.git",
|
"url": "https://github.com/codeigniter4/CodeIgniter4.git",
|
||||||
"reference": "9204aef421921f2c07021dda418ebfc200fe4a31"
|
"reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9204aef421921f2c07021dda418ebfc200fe4a31",
|
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/ccf68e1d7fc44bfe5abacc39bf16edae45794a83",
|
||||||
"reference": "9204aef421921f2c07021dda418ebfc200fe4a31",
|
"reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"phpstan/phpstan": "^0.12",
|
"phpstan/phpstan": "^0.12",
|
||||||
"phpunit/phpunit": "^8.5",
|
"phpunit/phpunit": "^8.5",
|
||||||
"predis/predis": "^1.1",
|
"predis/predis": "^1.1",
|
||||||
|
"rector/rector": "^0.8",
|
||||||
"squizlabs/php_codesniffer": "^3.3"
|
"squizlabs/php_codesniffer": "^3.3"
|
||||||
},
|
},
|
||||||
"type": "project",
|
"type": "project",
|
||||||
@ -45,6 +46,11 @@
|
|||||||
"CodeIgniter\\": "system/"
|
"CodeIgniter\\": "system/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Utils\\": "utils"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
"@composer dump-autoload",
|
"@composer dump-autoload",
|
||||||
@ -69,7 +75,7 @@
|
|||||||
"slack": "https://codeigniterchat.slack.com",
|
"slack": "https://codeigniterchat.slack.com",
|
||||||
"issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
|
"issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
|
||||||
},
|
},
|
||||||
"time": "2020-09-24T17:15:24+00:00"
|
"time": "2020-10-04T20:15:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "composer/ca-bundle",
|
"name": "composer/ca-bundle",
|
||||||
@ -143,27 +149,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "geoip2/geoip2",
|
"name": "geoip2/geoip2",
|
||||||
"version": "v2.10.0",
|
"version": "v2.11.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maxmind/GeoIP2-php.git",
|
"url": "https://github.com/maxmind/GeoIP2-php.git",
|
||||||
"reference": "419557cd21d9fe039721a83490701a58c8ce784a"
|
"reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/419557cd21d9fe039721a83490701a58c8ce784a",
|
"url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/d01be5894a5c1a3381c58c9b1795cd07f96c30f7",
|
||||||
"reference": "419557cd21d9fe039721a83490701a58c8ce784a",
|
"reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"maxmind-db/reader": "~1.5",
|
"maxmind-db/reader": "~1.8",
|
||||||
"maxmind/web-service-common": "~0.6",
|
"maxmind/web-service-common": "~0.8",
|
||||||
"php": ">=5.6"
|
"php": ">=7.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "2.*",
|
"friendsofphp/php-cs-fixer": "2.*",
|
||||||
"phpunit/phpunit": "5.*",
|
"phpunit/phpunit": "^8.0 || ^9.0",
|
||||||
"squizlabs/php_codesniffer": "3.*"
|
"squizlabs/php_codesniffer": "3.*"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
@ -192,7 +198,7 @@
|
|||||||
"geolocation",
|
"geolocation",
|
||||||
"maxmind"
|
"maxmind"
|
||||||
],
|
],
|
||||||
"time": "2019-12-12T18:48:39+00:00"
|
"time": "2020-10-01T18:48:34+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "graham-campbell/result-type",
|
"name": "graham-campbell/result-type",
|
||||||
@ -689,23 +695,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maxmind-db/reader",
|
"name": "maxmind-db/reader",
|
||||||
"version": "v1.7.0",
|
"version": "v1.8.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
|
"url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
|
||||||
"reference": "942553da239f12051275f9c666538b5dd09e2908"
|
"reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/942553da239f12051275f9c666538b5dd09e2908",
|
"url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/b566d429ac9aec10594b0935be8ff38302f8d5c8",
|
||||||
"reference": "942553da239f12051275f9c666538b5dd09e2908",
|
"reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2"
|
"php": ">=7.2"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
"ext-maxminddb": "<1.7.0,>=2.0.0"
|
"ext-maxminddb": "<1.8.0,>=2.0.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "2.*",
|
"friendsofphp/php-cs-fixer": "2.*",
|
||||||
@ -745,31 +751,31 @@
|
|||||||
"geolocation",
|
"geolocation",
|
||||||
"maxmind"
|
"maxmind"
|
||||||
],
|
],
|
||||||
"time": "2020-08-07T22:10:05+00:00"
|
"time": "2020-10-01T17:30:21+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "maxmind/web-service-common",
|
"name": "maxmind/web-service-common",
|
||||||
"version": "v0.7.0",
|
"version": "v0.8.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maxmind/web-service-common-php.git",
|
"url": "https://github.com/maxmind/web-service-common-php.git",
|
||||||
"reference": "74c996c218ada5c639c8c2f076756e059f5552fc"
|
"reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/74c996c218ada5c639c8c2f076756e059f5552fc",
|
"url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/ba67d9532cfaf499bd71774b8170d05df4f75fb7",
|
||||||
"reference": "74c996c218ada5c639c8c2f076756e059f5552fc",
|
"reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"composer/ca-bundle": "^1.0.3",
|
"composer/ca-bundle": "^1.0.3",
|
||||||
"ext-curl": "*",
|
"ext-curl": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"php": ">=5.6"
|
"php": ">=7.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"friendsofphp/php-cs-fixer": "2.*",
|
"friendsofphp/php-cs-fixer": "2.*",
|
||||||
"phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0",
|
"phpunit/phpunit": "^8.0 || ^9.0",
|
||||||
"squizlabs/php_codesniffer": "3.*"
|
"squizlabs/php_codesniffer": "3.*"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
@ -791,7 +797,7 @@
|
|||||||
],
|
],
|
||||||
"description": "Internal MaxMind Web Service API",
|
"description": "Internal MaxMind Web Service API",
|
||||||
"homepage": "https://github.com/maxmind/web-service-common-php",
|
"homepage": "https://github.com/maxmind/web-service-common-php",
|
||||||
"time": "2020-05-06T14:07:26+00:00"
|
"time": "2020-10-01T15:28:36+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "myth/auth",
|
"name": "myth/auth",
|
||||||
@ -918,6 +924,76 @@
|
|||||||
],
|
],
|
||||||
"time": "2020-07-20T17:29:33+00:00"
|
"time": "2020-07-20T17:29:33+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "podlibre/ipcat",
|
||||||
|
"version": "dev-master",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/podlibre/ipcat.git",
|
||||||
|
"reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/podlibre/ipcat/zipball/1adfc821be508ddc8a742f6a5d5e6e42fdf28e86",
|
||||||
|
"reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Podlibre\\Ipcat\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-3.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Benjamin Bellamy",
|
||||||
|
"email": "ben@podlibre.org",
|
||||||
|
"homepage": "https://podlibre.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Categorization of IP Addresses forked from https://github.com/client9/ipcat",
|
||||||
|
"homepage": "https://github.com/podlibre/ipcat",
|
||||||
|
"time": "2020-10-05T17:15:07+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "podlibre/user-agents-php",
|
||||||
|
"version": "dev-main",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/podlibre/user-agents-php.git",
|
||||||
|
"reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/podlibre/user-agents-php/zipball/891066bae6b4881a8b7a57eb72a67fca1fcf67c0",
|
||||||
|
"reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Podlibre\\UserAgentsPhp\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Benjamin Bellamy",
|
||||||
|
"email": "ben@podlibre.org",
|
||||||
|
"homepage": "https://podlibre.org/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP implementation for opawg/user-agents.",
|
||||||
|
"homepage": "https://github.com/podlibre/user-agents-php",
|
||||||
|
"time": "2020-10-05T16:58:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "psr/cache",
|
"name": "psr/cache",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -1801,28 +1877,28 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpspec/prophecy",
|
"name": "phpspec/prophecy",
|
||||||
"version": "1.11.1",
|
"version": "1.12.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpspec/prophecy.git",
|
"url": "https://github.com/phpspec/prophecy.git",
|
||||||
"reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
|
"reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
|
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
|
||||||
"reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
|
"reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"doctrine/instantiator": "^1.2",
|
"doctrine/instantiator": "^1.2",
|
||||||
"php": "^7.2",
|
"php": "^7.2 || ~8.0, <8.1",
|
||||||
"phpdocumentor/reflection-docblock": "^5.0",
|
"phpdocumentor/reflection-docblock": "^5.2",
|
||||||
"sebastian/comparator": "^3.0 || ^4.0",
|
"sebastian/comparator": "^3.0 || ^4.0",
|
||||||
"sebastian/recursion-context": "^3.0 || ^4.0"
|
"sebastian/recursion-context": "^3.0 || ^4.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpspec/phpspec": "^6.0",
|
"phpspec/phpspec": "^6.0",
|
||||||
"phpunit/phpunit": "^8.0"
|
"phpunit/phpunit": "^8.0 || ^9.0 <9.3"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
@ -1860,7 +1936,7 @@
|
|||||||
"spy",
|
"spy",
|
||||||
"stub"
|
"stub"
|
||||||
],
|
],
|
||||||
"time": "2020-07-08T12:44:21+00:00"
|
"time": "2020-09-29T09:10:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
|
@ -104,6 +104,13 @@ docker ps -a
|
|||||||
docker-compose run --rm app php spark migrate -all
|
docker-compose run --rm app php spark migrate -all
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In case you need to roll back, use this command:
|
||||||
|
|
||||||
|
```
|
||||||
|
# rolls back database schema loading (deletes all tables and their content)
|
||||||
|
docker-compose run --rm app php spark migrate:rollback
|
||||||
|
```
|
||||||
|
|
||||||
2. Populate the database with the required data:
|
2. Populate the database with the required data:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
1448
package-lock.json
generated
@ -24,6 +24,8 @@
|
|||||||
"commit": "git-cz"
|
"commit": "git-cz"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@amcharts/amcharts4": "^4.9.37",
|
||||||
|
"@amcharts/amcharts4-geodata": "^4.1.17",
|
||||||
"@popperjs/core": "^2.5.3",
|
"@popperjs/core": "^2.5.3",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
"prosemirror-example-setup": "^1.1.2",
|
"prosemirror-example-setup": "^1.1.2",
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 2.6 KiB |