@@ -28,6 +28,7 @@ public partial class FaviconService : IDisposable
28
28
private readonly LocalFaviconExtractor _localExtractor ;
29
29
private readonly CancellationTokenSource _cts = new ( ) ;
30
30
private readonly ConcurrentDictionary < string , Task < string ? > > _ongoingFetches = new ( StringComparer . OrdinalIgnoreCase ) ;
31
+ private readonly ConcurrentDictionary < string , DateTime > _failedFetches = new ( StringComparer . OrdinalIgnoreCase ) ;
31
32
32
33
[ GeneratedRegex ( "<link[^>]+?>" , RegexOptions . IgnoreCase | RegexOptions . Compiled , "en-US" ) ]
33
34
private static partial Regex LinkTagRegex ( ) ;
@@ -44,6 +45,7 @@ private record struct FaviconCandidate(string Url, int Score);
44
45
private record struct FetchResult ( string ? TempPath , int Size ) ;
45
46
private const int MaxFaviconBytes = 250 * 1024 ;
46
47
private const int TargetIconSize = 48 ;
48
+ private static readonly TimeSpan FailedFaviconCooldown = TimeSpan . FromHours ( 12 ) ; // How long to wait before retrying a failed favicon
47
49
48
50
public FaviconService ( PluginInitContext context , Settings settings , string tempPath )
49
51
{
@@ -114,6 +116,15 @@ await Parallel.ForEachAsync(bookmarks, options, async (bookmark, token) =>
114
116
return Task . FromResult < string ? > ( null ) ;
115
117
}
116
118
var authority = url . GetLeftPart ( UriPartial . Authority ) ;
119
+
120
+ // Check if this domain recently failed to provide a favicon
121
+ if ( _failedFetches . TryGetValue ( authority , out var lastAttemptTime ) &&
122
+ ( DateTime . UtcNow - lastAttemptTime < FailedFaviconCooldown ) )
123
+ {
124
+ _context . API . LogDebug ( nameof ( FaviconService ) , $ "Skipping favicon fetch for { authority } due to recent failure (cooldown active).") ;
125
+ return Task . FromResult < string ? > ( null ) ;
126
+ }
127
+
117
128
return _ongoingFetches . GetOrAdd ( authority , key => FetchAndCacheFaviconAsync ( new Uri ( key ) ) ) ;
118
129
}
119
130
@@ -139,6 +150,7 @@ private static string GetCachePath(string url, string cacheDir)
139
150
140
151
FetchResult icoResult = default ;
141
152
FetchResult htmlResult = default ;
153
+ bool fetchAttempted = false ;
142
154
143
155
try
144
156
{
@@ -156,6 +168,7 @@ private static string GetCachePath(string url, string cacheDir)
156
168
{
157
169
// A task finished with a good icon. We can cancel the other one.
158
170
linkedCts . Cancel ( ) ;
171
+ fetchAttempted = true ; // Mark as attempted, but potentially successful
159
172
break ;
160
173
}
161
174
}
@@ -170,6 +183,7 @@ private static string GetCachePath(string url, string cacheDir)
170
183
{
171
184
File . Move ( bestResult . TempPath , cachePath , true ) ;
172
185
_context . API . LogDebug ( nameof ( FaviconService ) , $ "Favicon for { urlString } cached successfully.") ;
186
+ _failedFetches . TryRemove ( urlString , out _ ) ; // Remove from blacklist on success
173
187
return cachePath ;
174
188
}
175
189
@@ -178,12 +192,19 @@ private static string GetCachePath(string url, string cacheDir)
178
192
catch ( Exception ex )
179
193
{
180
194
_context . API . LogException ( nameof ( FaviconService ) , $ "Error in favicon fetch for { urlString } ", ex ) ;
195
+ fetchAttempted = true ; // Mark as attempted and failed
181
196
}
182
197
finally
183
198
{
184
199
if ( icoResult . TempPath != null && File . Exists ( icoResult . TempPath ) ) File . Delete ( icoResult . TempPath ) ;
185
200
if ( htmlResult . TempPath != null && File . Exists ( htmlResult . TempPath ) ) File . Delete ( htmlResult . TempPath ) ;
186
201
_ongoingFetches . TryRemove ( urlString , out _ ) ;
202
+
203
+ // If fetch was attempted but no favicon found or an exception occurred, add to failed fetches
204
+ if ( fetchAttempted && ! File . Exists ( cachePath ) )
205
+ {
206
+ _failedFetches [ urlString ] = DateTime . UtcNow ;
207
+ }
187
208
}
188
209
189
210
return null ;
@@ -521,6 +542,7 @@ public void Dispose()
521
542
{
522
543
_cts . Cancel ( ) ;
523
544
_ongoingFetches . Clear ( ) ;
545
+ _failedFetches . Clear ( ) ; // Clear failed fetches on dispose
524
546
_httpClient . Dispose ( ) ;
525
547
_cts . Dispose ( ) ;
526
548
GC . SuppressFinalize ( this ) ;
0 commit comments