From 7ab700a7a476d742d27136af1e586b4a6db678ba Mon Sep 17 00:00:00 2001 From: Jason Morris Date: Tue, 10 Feb 2026 18:34:40 -0500 Subject: [PATCH 1/2] Track YouTube thumbnail 404 failures (Fixes AGGRO-1J) Add persistent tracking of thumbnail fetch failures per video. Each 404 increments a counter; successful fetches reset it. Videos exceeding 10 failures are flagged bad and removed from active rotation. Adds thumbnail_issue_count column, httpStatus pass-through in fetch_url/fetch_thumbnail, and flagBrokenThumbnails cleanup in the sweep cycle. --- aggro-db.sql | 1 + app/Controllers/Aggro.php | 5 + ...26-02-10-200000_AddThumbnailIssueCount.php | 24 ++++ app/Helpers/aggro_helper.php | 43 +++--- app/Models/AggroModels.php | 10 ++ app/Services/ThumbnailService.php | 69 +++++++++- .../20240101000001_CreateTestTables.php | 11 ++ tests/unit/AggroHelperTest.php | 37 +++++ tests/unit/ThumbnailServiceTest.php | 127 ++++++++++++++++++ 9 files changed, 306 insertions(+), 21 deletions(-) create mode 100644 app/Database/Migrations/2026-02-10-200000_AddThumbnailIssueCount.php diff --git a/aggro-db.sql b/aggro-db.sql index 450c4de4..c0c86ed3 100644 --- a/aggro-db.sql +++ b/aggro-db.sql @@ -71,6 +71,7 @@ CREATE TABLE `aggro_videos` ( `flag_bad` int(1) NOT NULL DEFAULT '0', `flag_favorite` int(1) NOT NULL DEFAULT '0', `notes` text, + `thumbnail_issue_count` int NOT NULL DEFAULT '0', PRIMARY KEY (`aggro_id`), UNIQUE KEY `videoid` (`video_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/app/Controllers/Aggro.php b/app/Controllers/Aggro.php index c503a9fc..eb7e054f 100644 --- a/app/Controllers/Aggro.php +++ b/app/Controllers/Aggro.php @@ -202,6 +202,11 @@ public function getSweep(): bool|ResponseInterface echo "\nThumbnails cleaned up.\n"; } + $flagged = $aggroModel->flagBrokenThumbnails(); + if ($flagged > 0) { + echo "\n" . $flagged . " broken thumbnail(s) flagged.\n"; + } + return true; } diff --git a/app/Database/Migrations/2026-02-10-200000_AddThumbnailIssueCount.php b/app/Database/Migrations/2026-02-10-200000_AddThumbnailIssueCount.php new file mode 100644 index 00000000..113b8782 --- /dev/null +++ b/app/Database/Migrations/2026-02-10-200000_AddThumbnailIssueCount.php @@ -0,0 +1,24 @@ +forge->addColumn('aggro_videos', [ + 'thumbnail_issue_count' => [ + 'type' => 'INT', + 'null' => false, + 'default' => 0, + ], + ]); + } + + public function down() + { + $this->forge->dropColumn('aggro_videos', 'thumbnail_issue_count'); + } +} diff --git a/app/Helpers/aggro_helper.php b/app/Helpers/aggro_helper.php index fa7ae0ad..6c3d7b3b 100644 --- a/app/Helpers/aggro_helper.php +++ b/app/Helpers/aggro_helper.php @@ -199,20 +199,22 @@ function fetch_feed(string $feed, bool|int $spoof, ?int $cache = null) /** * Fetch thumbnail image from video provider, process image, and save locally. * - * @param string $videoid - * The videoid. - * @param string $thumbnail - * The remote URL of the video thumbnail. + * @param string $videoid + * The videoid. + * @param string $thumbnail + * The remote URL of the video thumbnail. + * @param int|null &$httpStatus + * Optional. Populated with the HTTP status code from the fetch. * * @return bool * Video thumbnail fetched and processed. */ - function fetch_thumbnail($videoid, $thumbnail) + function fetch_thumbnail($videoid, $thumbnail, &$httpStatus = null) { helper('aggro'); $storageConfig = config('Storage'); $path = $storageConfig->getThumbnailPath($videoid); - $buffer = fetch_url($thumbnail); + $buffer = fetch_url($thumbnail, 'text', 0, $httpStatus); if (! empty($buffer)) { $file = fopen($path, 'wb'); @@ -259,20 +261,22 @@ function fetch_thumbnail($videoid, $thumbnail) /** * Fetch contents of URL (via CURL). Decode if XML or JSON. * - * @param string $url - * URL to be fetched. - * @param string $format - * Format to be returned: - * - text: return as text, no decoding. - * - simplexml: return as decoded XML. - * - json: return as decoded JSON. - * @param string $spoof - * Spoof user agent string (1/0). + * @param string $url + * URL to be fetched. + * @param string $format + * Format to be returned: + * - text: return as text, no decoding. + * - simplexml: return as decoded XML. + * - json: return as decoded JSON. + * @param string $spoof + * Spoof user agent string (1/0). + * @param int|null &$httpStatus + * Optional. Populated with the HTTP response code. * * @return string * Contents of requested url with optional decoding. */ - function fetch_url($url, $format = 'text', $spoof = 0) + function fetch_url($url, $format = 'text', $spoof = 0, &$httpStatus = null) { $storageConfig = config('Storage'); $agent = env('UA_BMXFEED', 'Aggro/1.0'); @@ -286,9 +290,10 @@ function fetch_url($url, $format = 'text', $spoof = 0) curl_setopt($fetch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($fetch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($fetch, CURLOPT_MAXREDIRS, $storageConfig->urlMaxRedirects); - $response = curl_exec($fetch); - $httpCode = curl_getinfo($fetch, CURLINFO_HTTP_CODE); - $errorInfo = curl_error($fetch); + $response = curl_exec($fetch); + $httpCode = curl_getinfo($fetch, CURLINFO_HTTP_CODE); + $httpStatus = $httpCode; + $errorInfo = curl_error($fetch); curl_close($fetch); if ($httpCode === 403 || $httpCode === 404 || $httpCode === 500) { diff --git a/app/Models/AggroModels.php b/app/Models/AggroModels.php index 34ba9d9d..a693028e 100644 --- a/app/Models/AggroModels.php +++ b/app/Models/AggroModels.php @@ -91,6 +91,16 @@ public function cleanThumbs() return $this->thumbnailService->cleanThumbs(); } + /** + * Flag videos with chronic thumbnail failures as bad. + * + * @return int Number of videos flagged + */ + public function flagBrokenThumbnails() + { + return $this->thumbnailService->flagBrokenThumbnails(); + } + /** * Get list of video channels that haven't been updated within timeframe. * diff --git a/app/Services/ThumbnailService.php b/app/Services/ThumbnailService.php index 6b64b20a..93d368e7 100644 --- a/app/Services/ThumbnailService.php +++ b/app/Services/ThumbnailService.php @@ -50,10 +50,16 @@ public function checkThumbs() continue; } - $message = $thumb->video_id . ' missing thumbnail'; - if (fetch_thumbnail($thumb->video_id, $thumb->video_thumbnail_url)) { + $httpStatus = null; + $message = $thumb->video_id . ' missing thumbnail'; + + if (fetch_thumbnail($thumb->video_id, $thumb->video_thumbnail_url, $httpStatus)) { $message .= ' — fetched.'; + $this->resetThumbnailIssueCount($thumb->video_id); + } elseif ($httpStatus === 404) { + $this->incrementThumbnailIssueCount($thumb->video_id); } + $this->utilityModel->sendLog($message); } @@ -63,6 +69,40 @@ public function checkThumbs() return true; } + /** + * Flag videos with chronic thumbnail failures as bad. + * + * Videos with thumbnail_issue_count above the threshold + * are permanently flagged bad so they stop being fetched, + * displayed, and retried. + * + * @return int Number of videos flagged + */ + public function flagBrokenThumbnails() + { + $threshold = 10; + + $videos = $this->db->table('aggro_videos') + ->select('video_id') + ->where('thumbnail_issue_count >', $threshold) + ->where('flag_bad', 0) + ->get() + ->getResult(); + + $count = 0; + + foreach ($videos as $video) { + $this->db->table('aggro_videos') + ->where('video_id', $video->video_id) + ->update(['flag_bad' => 1]); + + log_message('error', 'Flagged video ' . $video->video_id . ' as bad — thumbnail 404 count exceeded threshold (' . $threshold . ').'); + $count++; + } + + return $count; + } + /** * Clean thumbnail directory. * @@ -161,4 +201,29 @@ private function logCleanupResults($deletedCount, $errorCount) $this->utilityModel->sendLog($message); } + + /** + * Increment the thumbnail issue count for a video. + * + * @param string $videoId The video ID + */ + private function incrementThumbnailIssueCount($videoId) + { + $this->db->table('aggro_videos') + ->where('video_id', $videoId) + ->set('thumbnail_issue_count', 'thumbnail_issue_count + 1', false) + ->update(); + } + + /** + * Reset the thumbnail issue count for a video. + * + * @param string $videoId The video ID + */ + private function resetThumbnailIssueCount($videoId) + { + $this->db->table('aggro_videos') + ->where('video_id', $videoId) + ->update(['thumbnail_issue_count' => 0]); + } } diff --git a/tests/_support/Database/Migrations/20240101000001_CreateTestTables.php b/tests/_support/Database/Migrations/20240101000001_CreateTestTables.php index a8805737..86d06926 100644 --- a/tests/_support/Database/Migrations/20240101000001_CreateTestTables.php +++ b/tests/_support/Database/Migrations/20240101000001_CreateTestTables.php @@ -106,6 +106,17 @@ public function up() 'null' => false, 'default' => 0, ], + 'flag_favorite' => [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'null' => false, + 'default' => 0, + ], + 'thumbnail_issue_count' => [ + 'type' => 'INT', + 'null' => false, + 'default' => 0, + ], ]); $this->forge->addKey('aggro_id', true); $this->forge->addUniqueKey('video_id'); diff --git a/tests/unit/AggroHelperTest.php b/tests/unit/AggroHelperTest.php index 04e1a3ad..34884c34 100644 --- a/tests/unit/AggroHelperTest.php +++ b/tests/unit/AggroHelperTest.php @@ -345,4 +345,41 @@ public function testDecodeEntitiesWithNamedEntities(): void $result = decode_entities('Test & "quoted"'); $this->assertSame('Test & "quoted"', $result); } + + public function testFetchUrlPopulatesHttpStatus(): void + { + $httpStatus = null; + // Use an invalid URL that will get a cURL error (no HTTP code) + fetch_url('http://localhost:1', 'text', 0, $httpStatus); + + // httpStatus should be set (0 for connection failure) + $this->assertIsInt($httpStatus); + } + + public function testFetchUrlHttpStatusSetOn404(): void + { + $httpStatus = null; + // httpbin returns 404 for this endpoint + fetch_url('https://httpbin.org/status/404', 'text', 0, $httpStatus); + + $this->assertSame(404, $httpStatus); + } + + public function testFetchThumbnailPassesThroughHttpStatus(): void + { + $httpStatus = null; + // Fetching a non-existent thumbnail should populate httpStatus + fetch_thumbnail('nonexistent_video', 'https://httpbin.org/status/404', $httpStatus); + + $this->assertIsInt($httpStatus); + $this->assertSame(404, $httpStatus); + } + + public function testFetchUrlHttpStatusBackwardCompatible(): void + { + // Calling without the httpStatus param should still work + $result = fetch_url('http://localhost:1'); + // Should not throw any errors — backward-compatible + $this->assertFalse($result); + } } diff --git a/tests/unit/ThumbnailServiceTest.php b/tests/unit/ThumbnailServiceTest.php index e660229b..3dfcec62 100644 --- a/tests/unit/ThumbnailServiceTest.php +++ b/tests/unit/ThumbnailServiceTest.php @@ -297,4 +297,131 @@ public function testServiceUsesCorrectDependencies() $this->assertIsBool($result1); $this->assertIsBool($result2); } + + public function testIncrementThumbnailIssueCount() + { + // Arrange + $video = $this->makeTestVideo('inc_test', ['thumbnail_issue_count' => 0]); + $this->insertTestVideo($video); + + // Act + $reflection = new ReflectionClass($this->service); + $method = $reflection->getMethod('incrementThumbnailIssueCount'); + $method->setAccessible(true); + $method->invoke($this->service, 'inc_test'); + + // Assert + $row = $this->db->table('aggro_videos') + ->where('video_id', 'inc_test') + ->get() + ->getRow(); + + $this->assertSame(1, $row->thumbnail_issue_count); + } + + public function testResetThumbnailIssueCount() + { + // Arrange + $video = $this->makeTestVideo('reset_test', ['thumbnail_issue_count' => 5]); + $this->insertTestVideo($video); + + // Act + $reflection = new ReflectionClass($this->service); + $method = $reflection->getMethod('resetThumbnailIssueCount'); + $method->setAccessible(true); + $method->invoke($this->service, 'reset_test'); + + // Assert + $row = $this->db->table('aggro_videos') + ->where('video_id', 'reset_test') + ->get() + ->getRow(); + + $this->assertSame(0, $row->thumbnail_issue_count); + } + + public function testFlagBrokenThumbnails() + { + // Arrange — one video above threshold, one below + $highCount = $this->makeTestVideo('high_count', ['thumbnail_issue_count' => 11, 'flag_bad' => 0]); + $lowCount = $this->makeTestVideo('low_count', ['thumbnail_issue_count' => 3, 'flag_bad' => 0]); + $this->insertTestVideo($highCount); + $this->insertTestVideo($lowCount); + + // Act + $this->service->flagBrokenThumbnails(); + + // Assert — high count should be flagged bad + $highRow = $this->db->table('aggro_videos') + ->where('video_id', 'high_count') + ->get() + ->getRow(); + $this->assertSame(1, $highRow->flag_bad); + + // Assert — low count should remain unflagged + $lowRow = $this->db->table('aggro_videos') + ->where('video_id', 'low_count') + ->get() + ->getRow(); + $this->assertSame(0, $lowRow->flag_bad); + } + + public function testFlagBrokenThumbnailsSkipsAlreadyBadVideos() + { + // Arrange — video already flagged bad with high count + $alreadyBad = $this->makeTestVideo('already_bad', [ + 'thumbnail_issue_count' => 15, + 'flag_bad' => 1, + ]); + $this->insertTestVideo($alreadyBad); + + // Act + $result = $this->service->flagBrokenThumbnails(); + + // Assert — method should return 0 flagged (it was already bad) + $this->assertSame(0, $result); + } + + public function testFlagBrokenThumbnailsReturnsCount() + { + // Arrange + $video1 = $this->makeTestVideo('flag_count_1', ['thumbnail_issue_count' => 12, 'flag_bad' => 0]); + $video2 = $this->makeTestVideo('flag_count_2', ['thumbnail_issue_count' => 15, 'flag_bad' => 0]); + $this->insertTestVideo($video1); + $this->insertTestVideo($video2); + + // Act + $result = $this->service->flagBrokenThumbnails(); + + // Assert + $this->assertSame(2, $result); + } + + /** + * Build a test video array with sensible defaults. + */ + private function makeTestVideo(string $videoId, array $overrides = []): array + { + return array_merge([ + 'video_id' => $videoId, + 'aggro_date_added' => date('Y-m-d H:i:s'), + 'aggro_date_updated' => date('Y-m-d H:i:s'), + 'video_date_uploaded' => date('Y-m-d H:i:s'), + 'flag_archive' => 0, + 'flag_bad' => 0, + 'flag_favorite' => 0, + 'thumbnail_issue_count' => 0, + 'video_plays' => 100, + 'video_title' => 'Test Video', + 'video_thumbnail_url' => 'https://example.com/thumb.jpg', + 'video_width' => 1920, + 'video_height' => 1080, + 'video_aspect_ratio' => '16:9', + 'video_duration' => 300, + 'video_source_id' => 'test_source', + 'video_source_username' => 'testuser', + 'video_source_url' => 'https://example.com/video', + 'video_type' => 'youtube', + ], $overrides); + } } From c8d3b9bb78210299faf3e39682863df4c1ec626d Mon Sep 17 00:00:00 2001 From: Jason Morris Date: Tue, 10 Feb 2026 18:46:07 -0500 Subject: [PATCH 2/2] Update dependencies --- package-lock.json | 89 +++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/package-lock.json b/package-lock.json index db66083b..9a076c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "postcss": "8.5.6", "postcss-cli": "11.0.1", "prettier": "3.8.1", - "stylelint": "17.1.0", + "stylelint": "17.2.0", "stylelint-config-standard": "40.0.0" } }, @@ -77,6 +77,30 @@ "keyv": "^5.5.5" } }, + "node_modules/@csstools/css-calc": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.0.1.tgz", + "integrity": "sha512-bsDKIP6f4ta2DO9t+rAbSSwv4EMESXy5ZIvzQl1afmD6Z1XHkVu9ijcG9QR/qSgQS1dVa+RaQ/MfQ7FIB/Dn1Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", @@ -93,7 +117,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -102,9 +125,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", - "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", "dev": true, "funding": [ { @@ -116,10 +139,7 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } + "license": "MIT-0" }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -137,7 +157,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -466,7 +485,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1426,7 +1444,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -1665,7 +1682,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2171,7 +2187,6 @@ "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2483,9 +2498,9 @@ } }, "node_modules/stylelint": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.1.0.tgz", - "integrity": "sha512-+cUX1FxkkbLX5qJRAPapUv/+v+YU3pGbWu+pHVqTXpiY0mYh3Dxfxa0bLBtVtYgOC8hIWIyX2H/3Y3LWlAevDg==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.2.0.tgz", + "integrity": "sha512-602jhMkRt6P1dSh9kEzbFIaOKY//h4D0E7u/w2WHKxmi5VAjjMqe6P8rQPJuCWdbB3apOkjOFN5kcg6qWPIZWQ==", "dev": true, "funding": [ { @@ -2497,10 +2512,11 @@ "url": "https://github.com/sponsors/stylelint" } ], - "peer": true, + "license": "MIT", "dependencies": { + "@csstools/css-calc": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.25", + "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -2786,7 +2802,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3036,26 +3051,31 @@ "keyv": "^5.5.5" } }, + "@csstools/css-calc": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.0.1.tgz", + "integrity": "sha512-bsDKIP6f4ta2DO9t+rAbSSwv4EMESXy5ZIvzQl1afmD6Z1XHkVu9ijcG9QR/qSgQS1dVa+RaQ/MfQ7FIB/Dn1Q==", + "dev": true, + "requires": {} + }, "@csstools/css-parser-algorithms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, - "peer": true, "requires": {} }, "@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz", - "integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==", + "version": "1.0.27", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.27.tgz", + "integrity": "sha512-sxP33Jwg1bviSUXAV43cVYdmjt2TLnLXNqCWl9xmxHawWVjGz/kEbdkr7F9pxJNBN2Mh+dq0crgItbW6tQvyow==", "dev": true }, "@csstools/css-tokenizer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "peer": true + "dev": true }, "@csstools/media-query-list-parser": { "version": "5.0.0", @@ -3235,7 +3255,6 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "peer": true, "requires": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3922,7 +3941,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "peer": true, "requires": { "@keyv/serialize": "^1.1.1" } @@ -4080,7 +4098,6 @@ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, - "peer": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4372,7 +4389,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4567,14 +4583,14 @@ } }, "stylelint": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.1.0.tgz", - "integrity": "sha512-+cUX1FxkkbLX5qJRAPapUv/+v+YU3pGbWu+pHVqTXpiY0mYh3Dxfxa0bLBtVtYgOC8hIWIyX2H/3Y3LWlAevDg==", + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.2.0.tgz", + "integrity": "sha512-602jhMkRt6P1dSh9kEzbFIaOKY//h4D0E7u/w2WHKxmi5VAjjMqe6P8rQPJuCWdbB3apOkjOFN5kcg6qWPIZWQ==", "dev": true, - "peer": true, "requires": { + "@csstools/css-calc": "^3.0.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.25", + "@csstools/css-syntax-patches-for-csstree": "^1.0.26", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", @@ -4750,8 +4766,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "peer": true + "dev": true } } }, diff --git a/package.json b/package.json index 76534bc8..a8e8b276 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "postcss": "8.5.6", "postcss-cli": "11.0.1", "prettier": "3.8.1", - "stylelint": "17.1.0", + "stylelint": "17.2.0", "stylelint-config-standard": "40.0.0" } }