diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index 3482b1eb..7066c5e3 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -55,7 +55,15 @@ class Analytics extends Controller ) { helper('media'); - podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize); + $serviceName = isset($_GET['s']) ? $_GET['s'] : ''; + + podcast_hit( + $podcastId, + $episodeId, + $bytesThreshold, + $fileSize, + $serviceName + ); return redirect()->to(media_url(implode('/', $filename))); } } diff --git a/app/Controllers/Feed.php b/app/Controllers/Feed.php index a739a793..3da87f0d 100644 --- a/app/Controllers/Feed.php +++ b/app/Controllers/Feed.php @@ -15,13 +15,32 @@ class Feed extends Controller { public function index($podcastName) { - // The page cache is set to a decade so it is deleted manually upon podcast update - $this->cachePage(DECADE); - helper('rss'); $podcast = (new PodcastModel())->where('name', $podcastName)->first(); + if (!$podcast) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } - return $this->response->setXML(get_rss_feed($podcast)); + $service = null; + try { + $service = \Opawg\UserAgentsPhp\UserAgentsRSS::find( + $_SERVER['HTTP_USER_AGENT'] + ); + } catch (\Exception $e) { + // If things go wrong the show must go on and the user must be able to download the file + log_message('critical', $e); + } + $cacheName = + "podcast{$podcast->id}_feed" . + ($service ? "_{$service['slug']}" : ''); + if (!($found = cache($cacheName))) { + $found = get_rss_feed( + $podcast, + $service ? '?s=' . urlencode($service['name']) : '' + ); + cache()->save($cacheName, $found, DECADE); + } + return $this->response->setXML($found); } } diff --git a/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php index a8f8d32d..192ebd07 100644 --- a/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php +++ b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php @@ -25,6 +25,10 @@ class AddAnalyticsPodcastsByPlayer extends Migration 'date' => [ 'type' => 'date', ], + 'service' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + ], 'app' => [ 'type' => 'VARCHAR', 'constraint' => 128, @@ -51,6 +55,7 @@ class AddAnalyticsPodcastsByPlayer extends Migration $this->forge->addPrimaryKey([ 'podcast_id', 'date', + 'service', 'app', 'device', 'os', diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php index 7089e51e..9978a59f 100644 --- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php +++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php @@ -28,6 +28,7 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` ( IN `p_region_code` VARCHAR(3) CHARSET utf8mb4, IN `p_latitude` FLOAT, IN `p_longitude` FLOAT, + IN `p_service` VARCHAR(128) CHARSET utf8mb4, IN `p_app` VARCHAR(128) CHARSET utf8mb4, IN `p_device` VARCHAR(32) CHARSET utf8mb4, IN `p_os` VARCHAR(32) CHARSET utf8mb4, @@ -52,8 +53,8 @@ IF NOT `p_bot` THEN 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())) +INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `service`, `app`, `device`, `os`, `bot`, `date`) + VALUES (p_podcast_id, p_service, p_app, p_device, p_os, p_bot, DATE(NOW())) ON DUPLICATE KEY UPDATE `hits`=`hits`+1; END EOD; diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index 5c242d27..1eef6ecc 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -102,7 +102,7 @@ function set_user_session_player() $userAgent = $_SERVER['HTTP_USER_AGENT']; try { - $playerFound = \Podlibre\UserAgentsPhp\UserAgents::find($userAgent); + $playerFound = \Opawg\UserAgentsPhp\UserAgents::find($userAgent); } catch (\Exception $e) { // If things go wrong the show must go on and the user must be able to download the file } @@ -227,6 +227,7 @@ function webpage_hit($podcast_id) * ✅ Castopod does not do pre-load * ✅ IP deny list https://github.com/client9/ipcat * ✅ User-agent Filtering https://github.com/opawg/user-agents + * ✅ RSS User-agent https://github.com/opawg/podcast-rss-useragents * ✅ 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 @@ -234,11 +235,17 @@ function webpage_hit($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 + * @param string $serviceName The name of the service that had fetched the RSS feed * * @return void */ -function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) -{ +function podcast_hit( + $podcastId, + $episodeId, + $bytesThreshold, + $fileSize, + $serviceName +) { $session = \Config\Services::session(); $session->start(); @@ -328,22 +335,18 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) // We save the download count for this user until midnight: cache()->save($listenerHashId, $downloadsByUser, $midnightTTL); - $app = $session->get('player')['app']; - $device = $session->get('player')['device']; - $os = $session->get('player')['os']; - $bot = $session->get('player')['bot']; - - $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?);", [ + $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?);", [ $podcastId, $episodeId, $session->get('location')['countryCode'], $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, + $serviceName, + $session->get('player')['app'], + $session->get('player')['device'], + $session->get('player')['os'], + $session->get('player')['bot'], $newListener, ]); } diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 78413378..9627c3d1 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -13,9 +13,10 @@ use CodeIgniter\I18n\Time; * Generates the rss feed for a given podcast entity * * @param App\Entities\Podcast $podcast + * @param string $service The name of the service that fetches the RSS feed for future reference when the audio file is eventually downloaded * @return string rss feed as xml */ -function get_rss_feed($podcast) +function get_rss_feed($podcast, $serviceName = '') { $episodes = $podcast->episodes; @@ -102,7 +103,7 @@ function get_rss_feed($podcast) $item->addChild('title', $episode->title); $enclosure = $item->addChild('enclosure'); - $enclosure->addAttribute('url', $episode->enclosure_url); + $enclosure->addAttribute('url', $episode->enclosure_url . $serviceName); $enclosure->addAttribute('length', $episode->enclosure_filesize); $enclosure->addAttribute('type', $episode->enclosure_mimetype); diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php index ac27218e..206df556 100644 --- a/app/Language/en/Charts.php +++ b/app/Language/en/Charts.php @@ -7,22 +7,23 @@ */ return [ - 'by_player_weekly' => 'Podcast downloads by player (for the past week)', - 'by_player_yearly' => 'Podcast downloads by player (for the past year)', - 'by_device_weekly' => 'Podcast downloads by device (for the past week)', - 'by_os_weekly' => 'Podcast downloads by O.S. (for the past week)', - 'podcast_by_region' => 'Podcast downloads by region (for the past week)', + 'by_service_weekly' => 'Episode downloads by service (for the past week)', + 'by_player_weekly' => 'Episode downloads by player (for the past week)', + 'by_player_yearly' => 'Episode downloads by player (for the past year)', + 'by_device_weekly' => 'Episode downloads by device (for the past week)', + 'by_os_weekly' => 'Episode downloads by O.S. (for the past week)', + 'podcast_by_region' => 'Episode downloads by region (for the past week)', 'unique_daily_listeners' => 'Daily unique listeners', 'unique_monthly_listeners' => 'Monthly unique listeners', 'by_browser' => 'Web pages usage by browser (for the past week)', - 'podcast_by_day' => 'Podcast daily downloads', - 'podcast_by_month' => 'Podcast monthly downloads', + 'podcast_by_day' => 'Episode daily downloads', + 'podcast_by_month' => 'Episode monthly downloads', 'episode_by_day' => 'Episode daily downloads (first 60 days)', 'episode_by_month' => 'Episode monthly downloads', 'episodes_by_day' => '5 latest episodes downloads (during their first 60 days)', - 'by_country_weekly' => 'Podcast downloads by country (for the past week)', - 'by_country_yearly' => 'Podcast downloads by country (for the past year)', + 'by_country_weekly' => 'Episode downloads by country (for the past week)', + 'by_country_yearly' => 'Episode downloads by country (for the past year)', 'by_domain_weekly' => 'Web pages visits by source (for the past week)', 'by_domain_yearly' => 'Web pages visits by source (for the past year)', 'by_entry_page' => 'Web pages visits by landing page (for the past week)', diff --git a/app/Language/fr/Charts.php b/app/Language/fr/Charts.php index b878234d..14607d26 100644 --- a/app/Language/fr/Charts.php +++ b/app/Language/fr/Charts.php @@ -7,31 +7,33 @@ */ return [ + 'by_service_weekly' => + 'Téléchargements d’épisodes par service (sur la dernière semaine)', 'by_player_weekly' => - 'Téléchargements de Podcast par lecteur (sur la dernière semaine)', + 'Téléchargements d’épisodes par lecteur (sur la dernière semaine)', 'by_player_yearly' => - 'Téléchargements de Podcast par lecteur (sur la dernière année)', + 'Téléchargements d’épisodes par lecteur (sur la dernière année)', 'by_device_weekly' => - 'Téléchargements de Podcast par appareil (sur la dernière semaine)', + 'Téléchargements d’épisodes par appareil (sur la dernière semaine)', 'by_os_weekly' => - 'Téléchargements de Podcast par OS (sur la dernière semaine)', + 'Téléchargements d’épisodes par OS (sur la dernière semaine)', 'podcast_by_region' => - 'Téléchargements de Podcast par région (sur la dernière semaine)', + 'Téléchargements d’épisodes par région (sur la dernière semaine)', 'unique_daily_listeners' => 'Auditeurs uniques quotidiens', 'unique_monthly_listeners' => 'Auditeurs uniques mensuels', 'by_browser' => 'Fréquentation des pages web par navigateur (sur la dernière semaine)', - 'podcast_by_day' => 'Téléchargements quotidiens de podcasts', - 'podcast_by_month' => 'Téléchargements mensuels de podcasts', + 'podcast_by_day' => 'Téléchargements quotidiens d’épisodes', + 'podcast_by_month' => 'Téléchargements mensuels d’épisodes', 'episode_by_day' => 'Téléchargements quotidiens de l’épisode (sur les 60 premiers jours)', 'episode_by_month' => 'Téléchargements mensuels de l’épisode', 'episodes_by_day' => 'Téléchargements des 5 derniers épisodes (sur les 60 premiers jours)', 'by_country_weekly' => - 'Téléchargement de podcasts par pays (sur la dernière semaine)', + 'Téléchargement d’épisodes par pays (sur la dernière semaine)', 'by_country_yearly' => - 'Téléchargement de podcasts par pays (sur la dernière année)', + 'Téléchargement d’épisodes par pays (sur la dernière année)', 'by_domain_weekly' => 'Fréquentation des pages web par origine (sur la dernière semaine)', 'by_domain_yearly' => diff --git a/app/Models/AnalyticsPodcastByPlayerModel.php b/app/Models/AnalyticsPodcastByPlayerModel.php index bb03fcf8..1fb6b899 100644 --- a/app/Models/AnalyticsPodcastByPlayerModel.php +++ b/app/Models/AnalyticsPodcastByPlayerModel.php @@ -23,6 +23,41 @@ class AnalyticsPodcastByPlayerModel extends Model protected $useTimestamps = false; + /** + * Gets service data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByServiceWeekly(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcasts_by_player_by_service_weekly" + )) + ) { + $found = $this->select('`service` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`service` !=' => '', + '`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_service_weekly", + $found, + 600 + ); + } + return $found; + } + /** * Gets player data for a podcast * diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index eb1e1f99..2242266c 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -272,7 +272,12 @@ class EpisodeModel extends Model ); // delete cache for rss feed - cache()->delete(md5($episode->podcast->feed_url)); + cache()->delete("podcast{$episode->podcast_id}_feed"); + foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) { + cache()->delete( + "podcast{$episode->podcast_id}_feed_{$service['slug']}" + ); + } // delete model requests cache cache()->delete("podcast{$episode->podcast_id}_episodes"); diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index bb92d0e9..5c75640e 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -173,7 +173,10 @@ class PodcastModel extends Model $supportedLocales = config('App')->supportedLocales; // delete cache for rss feed and podcast pages - cache()->delete(md5($podcast->feed_url)); + cache()->delete("podcast{$podcast->id}_feed"); + foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) { + cache()->delete("podcast{$podcast->id}_feed_{$service['slug']}"); + } // delete model requests cache cache()->delete("podcast{$podcast->id}"); diff --git a/app/Views/admin/podcast/analytics/players.php b/app/Views/admin/podcast/analytics/players.php index 2a5fa59d..fcc2e83b 100644 --- a/app/Views/admin/podcast/analytics/players.php +++ b/app/Views/admin/podcast/analytics/players.php @@ -21,14 +21,13 @@ ) ?>"> -