diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index 3482b1eb6939fe694a05e32a1e81c09e5253c2d3..7066c5e340fbd4ca2e7ae373a665c418516778b8 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 a739a793b113965bd05c23c8f123f2599bf12560..3da87f0dd3bc177467851f154c2b8013d6ed46a4 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 a8f8d32d7d7866c481dea392a91a60d230eeee40..192ebd07fb70888e78f8eafc02b981ec068997bf 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 7089e51e627a18df346d6bd317848261ec9096cd..9978a59f0b003f99e4cd332e4ffd75079cb273a6 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 5c242d27c1fa6ae1c2cbf2193a8a9751bd8f182c..1eef6ecce352a016ad9908ff19a12f6d9b96a1ca 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 7841337834105ed685468c635754037f5a962544..9627c3d128688858b99a5b29951532a0abdf39fc 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 ac27218ebea87e31da682f9e80b78d5e0a664c5a..206df55635a9a7c244dbecb36281db44185ac473 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 b878234d1056c1e1f26da65d6cf3543cee8de2c7..14607d2690f70ad66014c2a5231396b8ab59a659 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 bb03fcf81ed2840bf4be5b0192ec154f720c30c6..1fb6b899b2b927dd041a73bfd27fd7d2da3263ec 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 eb1e1f990d7809a0488c43f96791b95ceeb23dbb..2242266c263cd61e21ea071c9d863e0e2b1139ae 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 bb92d0e9264d8c78f8e08dc9c974469204f6fe78..5c75640eba42403979ed195fe365851e76864743 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 2a5fa59dcde96678239e79efeaf829abf9fbc10c..fcc2e83bf7bdec3f1b305f8564b8c94f359bb8b5 100644 --- a/app/Views/admin/podcast/analytics/players.php +++ b/app/Views/admin/podcast/analytics/players.php @@ -21,14 +21,13 @@ ) ?>"></div> </div> - <div class="mb-12 mr-6 text-center"> - <h2><?= lang('Charts.by_player_yearly') ?></h2> - <div class="chart-pie" id="by-app-yearly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + <h2><?= lang('Charts.by_service_weekly') ?></h2> + <div class="chart-pie" id="by-service-weekly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( 'analytics-data', $podcast->id, 'PodcastByPlayer', - 'ByAppYearly' + 'ByServiceWeekly' ) ?>"></div> </div> diff --git a/app/Views/admin/podcast/create.php b/app/Views/admin/podcast/create.php index 1ee8414124f8c29eec5c51d2d81de85f168058b9..7b08bd8bf126ad723f036ff72ccc0e20626a76ba 100644 --- a/app/Views/admin/podcast/create.php +++ b/app/Views/admin/podcast/create.php @@ -27,6 +27,7 @@ 'id' => 'image', 'name' => 'image', 'class' => 'form-input', + 'required' => 'required', 'type' => 'file', 'accept' => '.jpg,.jpeg,.png', @@ -58,21 +59,27 @@ 'required' => 'required', ]) ?> -<?= form_fieldset('', [ - 'class' => 'mb-4', -]) ?> +<?= form_fieldset('', ['class' => 'mb-4']) ?> <legend> <?= lang('Podcast.form.type.label') . hint_tooltip(lang('Podcast.form.type.hint'), 'ml-1') ?> </legend> <?= form_radio( - ['id' => 'episodic', 'name' => 'type', 'class' => 'form-radio-btn'], + [ + 'id' => 'episodic', + 'name' => 'type', + 'class' => 'form-radio-btn', + ], 'episodic', old('type') ? old('type') == 'episodic' : true ) ?> <label for="episodic"><?= lang('Podcast.form.type.episodic') ?></label> <?= form_radio( - ['id' => 'serial', 'name' => 'type', 'class' => 'form-radio-btn'], + [ + 'id' => 'serial', + 'name' => 'type', + 'class' => 'form-radio-btn', + ], 'serial', old('type') ? old('type') == 'serial' : false ) ?> @@ -252,6 +259,7 @@ ['id' => 'block', 'name' => 'block'], 'yes', old('block', false), + 'mb-2' ) ?> @@ -266,7 +274,7 @@ lang('Podcast.form.lock'), ['id' => 'lock', 'name' => 'lock'], 'yes', - old('lock', $podcast->lock) + old('lock', true) ) ?> <?= form_section_close() ?> diff --git a/composer.json b/composer.json index 214a26640143a837b075e406cb1597366ed3b3af..b2bce3e852bce414497913df1e9fa5e0dd9f98e7 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "league/commonmark": "^1.5", "vlucas/phpdotenv": "^5.2", "league/html-to-markdown": "^4.10", - "podlibre/user-agents-php": "*", + "opawg/user-agents-php": "*", "podlibre/ipcat": "*" }, "require-dev": { @@ -31,12 +31,14 @@ "scripts": { "test": "phpunit", "post-install-cmd": [ - "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php", + "@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php > vendor/opawg/user-agents-php/src/UserAgents.php", + "@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php > vendor/opawg/user-agents-php/src/UserAgentsRSS.php", "@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php" ], "post-update-cmd": [ "@composer dump-autoload", - "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php", + "@php vendor/opawg/user-agents-php/src/UserAgentsGenerate.php > vendor/opawg/user-agents-php/src/UserAgents.php", + "@php vendor/opawg/user-agents-php/src/UserAgentsRSSGenerate.php > vendor/opawg/user-agents-php/src/UserAgentsRSS.php", "@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php" ] }, diff --git a/composer.lock b/composer.lock index 27374187ff5622623c5034f658eda192f2500d09..38bacea659f60947bc5a5000c46ea2f5b4b6ee24 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "47b9f628f03f8c494a9339b054359ec8", + "content-hash": "37551523e4097a9341bc00dd317f573d", "packages": [ { "name": "codeigniter4/codeigniter4", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/codeigniter4/CodeIgniter4.git", - "reference": "f5545aa7274575c397efae4ebcf6c18779dcf895" + "reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/f5545aa7274575c397efae4ebcf6c18779dcf895", - "reference": "f5545aa7274575c397efae4ebcf6c18779dcf895", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/13ff147fa4cd9db15888b041ef35bc22ed94252a", + "reference": "13ff147fa4cd9db15888b041ef35bc22ed94252a", "shasum": "" }, "require": { @@ -75,7 +75,7 @@ "slack": "https://codeigniterchat.slack.com", "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" }, - "time": "2020-10-06T06:38:58+00:00" + "time": "2020-10-20T18:13:11+00:00" }, { "name": "composer/ca-bundle", @@ -518,16 +518,16 @@ }, { "name": "league/commonmark", - "version": "1.5.5", + "version": "1.5.6", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "45832dfed6007b984c0d40addfac48d403dc6432" + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/45832dfed6007b984c0d40addfac48d403dc6432", - "reference": "45832dfed6007b984c0d40addfac48d403dc6432", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", "shasum": "" }, "require": { @@ -609,7 +609,7 @@ "type": "tidelift" } ], - "time": "2020-09-13T14:44:46+00:00" + "time": "2020-10-17T21:33:03+00:00" }, { "name": "league/html-to-markdown", @@ -805,12 +805,12 @@ "source": { "type": "git", "url": "https://github.com/lonnieezell/myth-auth.git", - "reference": "e838cb8de6ffa118caf2b9909e71776a866c8973" + "reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/e838cb8de6ffa118caf2b9909e71776a866c8973", - "reference": "e838cb8de6ffa118caf2b9909e71776a866c8973", + "url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/e9d6a2f557bd275158e0b84624534b2abeeb539c", + "reference": "e9d6a2f557bd275158e0b84624534b2abeeb539c", "shasum": "" }, "require": { @@ -818,9 +818,12 @@ }, "require-dev": { "codeigniter4/codeigniter4": "dev-develop", + "codeigniter4/codeigniter4-standard": "^1.0", "fzaninotto/faker": "^1.9@dev", "mockery/mockery": "^1.0", - "phpunit/phpunit": "8.5.*" + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "autoload": { @@ -857,7 +860,42 @@ "type": "patreon" } ], - "time": "2020-09-07T03:37:26+00:00" + "time": "2020-10-16T18:51:37+00:00" + }, + { + "name": "opawg/user-agents-php", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/opawg/user-agents-php.git", + "reference": "3b71eeed2c3216f1c1c361c62d4d3a7002be0481" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opawg/user-agents-php/zipball/3b71eeed2c3216f1c1c361c62d4d3a7002be0481", + "reference": "3b71eeed2c3216f1c1c361c62d4d3a7002be0481", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Opawg\\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/opawg/user-agents-php", + "time": "2020-10-20T23:22:20+00:00" }, { "name": "phpoption/phpoption", @@ -959,41 +997,6 @@ "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", "version": "1.0.1",