From 77711965c08b76971880a343b68aa545906cac4d Mon Sep 17 00:00:00 2001
From: Andy <88590076+AAndyProgram@users.noreply.github.com>
Date: Sat, 30 Sep 2023 09:16:57 +0300
Subject: [PATCH] 2023.9.30.0
Add Threads.net
API.UserDataBase: add 'EraseData_AdditionalDataFiles' function; add 'ThrowAnyImpl' function; add 'e' arg to 'LogError' function
API.Instagram: make classes compatible with threads.net; add top limits; update container parsers; add an override to the 'EraseData_AdditionalDataFiles' function
---
SCrawler/API/Base/UserDataBase.vb | 11 +-
SCrawler/API/Instagram/Declarations.vb | 2 +-
SCrawler/API/Instagram/SiteSettings.vb | 16 +-
SCrawler/API/Instagram/UserData.vb | 117 +++++--
SCrawler/API/ThreadsNet/SiteSettings.vb | 166 ++++++++++
SCrawler/API/ThreadsNet/UserData.vb | 290 ++++++++++++++++++
.../Icons/SiteIcons/ThreadsIcon_192.ico | Bin 0 -> 169662 bytes
.../PluginsEnvironment/Hosts/PluginHost.vb | 1 +
SCrawler/SCrawler.vbproj | 5 +
SCrawler/SiteResources.Designer.vb | 10 +
SCrawler/SiteResources.resx | 3 +
11 files changed, 580 insertions(+), 41 deletions(-)
create mode 100644 SCrawler/API/ThreadsNet/SiteSettings.vb
create mode 100644 SCrawler/API/ThreadsNet/UserData.vb
create mode 100644 SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico
diff --git a/SCrawler/API/Base/UserDataBase.vb b/SCrawler/API/Base/UserDataBase.vb
index a775605..445ae86 100644
--- a/SCrawler/API/Base/UserDataBase.vb
+++ b/SCrawler/API/Base/UserDataBase.vb
@@ -1829,6 +1829,7 @@ BlockNullPicture:
If m.Contains(IUserData.EraseMode.History) Then
If MyFilePosts.Delete(SFO.File, SFODelete.DeleteToRecycleBin, e) Then result = True
If MyFileData.Delete(SFO.File, SFODelete.DeleteToRecycleBin, e) Then result = True
+ EraseData_AdditionalDataFiles()
End If
If m.Contains(IUserData.EraseMode.Data) Then
Dim files As List(Of SFile) = SFile.GetFiles(DownloadContentDefault_GetRootDir.CSFileP,, SearchOption.AllDirectories, e)
@@ -1850,6 +1851,8 @@ BlockNullPicture:
Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, $"EraseData({CInt(Mode)}): {ToStringForLog()}", False)
End Try
End Function
+ Protected Overridable Sub EraseData_AdditionalDataFiles()
+ End Sub
Friend Overridable Function Delete(Optional ByVal Multiple As Boolean = False, Optional ByVal CollectionValue As Integer = -1) As Integer Implements IUserData.Delete
Dim f As SFile = SFile.GetPath(MyFile.CutPath.Path)
If f.Exists(SFO.Path, False) AndAlso (User.Merged OrElse f.Delete(SFO.Path, Settings.DeleteMode)) Then
@@ -2026,8 +2029,8 @@ BlockNullPicture:
End Function
#End Region
#Region "Errors functions"
- Protected Sub LogError(ByVal ex As Exception, ByVal Message As String)
- ErrorsDescriber.Execute(EDP.SendToLog, ex, $"{ToStringForLog()}: {Message}")
+ Protected Sub LogError(ByVal ex As Exception, ByVal Message As String, Optional ByVal e As ErrorsDescriber = Nothing)
+ ErrorsDescriber.Execute(If(e.Exists, e, New ErrorsDescriber(EDP.SendToLog)), ex, $"{ToStringForLog()}: {Message}")
End Sub
Protected Sub ErrorDownloading(ByVal f As SFile, ByVal URL As String)
If Not f.Exists Then MyMainLOG = $"Error downloading from [{URL}] to [{f}]"
@@ -2040,9 +2043,13 @@ BlockNullPicture:
Private Overloads Sub ThrowAny() Implements IThrower.ThrowAny
ThrowAny(TokenQueue)
End Sub
+ ''' ThrowAnyImpl(Token)
'''
'''
Friend Overridable Overloads Sub ThrowAny(ByVal Token As CancellationToken)
+ ThrowAnyImpl(Token)
+ End Sub
+ Protected Sub ThrowAnyImpl(ByVal Token As CancellationToken)
Token.ThrowIfCancellationRequested()
TokenQueue.ThrowIfCancellationRequested()
TokenPersonal.ThrowIfCancellationRequested()
diff --git a/SCrawler/API/Instagram/Declarations.vb b/SCrawler/API/Instagram/Declarations.vb
index e8bed4c..ae1e125 100644
--- a/SCrawler/API/Instagram/Declarations.vb
+++ b/SCrawler/API/Instagram/Declarations.vb
@@ -17,7 +17,7 @@ Namespace API.Instagram
Friend ReadOnly FilesPattern As RParams = RParams.DMS(".+?([^/\?]+?\.[\w\d]{3,4})(?=(\?|\Z))", 1, EDP.ReturnValue)
Friend Sub UpdateResponser(ByVal Source As IResponse, ByRef Destination As Responser)
Const r_wwwClaimName$ = "x-ig-set-www-claim"
- Const r_tokenName$ = "csrftoken"
+ Const r_tokenName$ = SiteSettings.Header_CSRF_TOKEN_COOKIE
If Not Source Is Nothing Then
Dim isInternal As Boolean = TypeOf Source Is WebDataResponse
Dim wwwClaimName$, tokenName$
diff --git a/SCrawler/API/Instagram/SiteSettings.vb b/SCrawler/API/Instagram/SiteSettings.vb
index d73a00a..888d8ab 100644
--- a/SCrawler/API/Instagram/SiteSettings.vb
+++ b/SCrawler/API/Instagram/SiteSettings.vb
@@ -70,13 +70,14 @@ Namespace API.Instagram
End Class
#End Region
#Region "Authorization properties"
- Private Const Header_IG_APP_ID As String = "x-ig-app-id"
+ Friend Const Header_IG_APP_ID As String = "x-ig-app-id"
Friend Const Header_IG_WWW_CLAIM As String = "x-ig-www-claim"
Friend Const Header_CSRF_TOKEN As String = "x-csrftoken"
- Private Const Header_ASBD_ID As String = "X-Asbd-Id"
- Private Const Header_Browser As String = "Sec-Ch-Ua"
- Private Const Header_BrowserExt As String = "Sec-Ch-Ua-Full-Version-List"
- Private Const Header_Platform As String = "Sec-Ch-Ua-Platform-Version"
+ Friend Const Header_CSRF_TOKEN_COOKIE As String = "csrftoken"
+ Friend Const Header_ASBD_ID As String = "X-Asbd-Id"
+ Friend Const Header_Browser As String = "Sec-Ch-Ua"
+ Friend Const Header_BrowserExt As String = "Sec-Ch-Ua-Full-Version-List"
+ Friend Const Header_Platform As String = "Sec-Ch-Ua-Platform-Version"
Friend ReadOnly Property HashTagged As PropertyValue
@@ -365,13 +366,16 @@ Namespace API.Instagram
SkipUntilNextSession = False
End Sub
#End Region
-#Region "UserOptions, GetUserPostUrl"
+#Region "UserOptions, GetUserUrl, GetUserPostUrl"
Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean)
If Options Is Nothing OrElse Not TypeOf Options Is EditorExchangeOptions Then Options = New EditorExchangeOptions(Me)
If OpenForm Then
Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using
End If
End Sub
+ Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String
+ Return String.Format(UrlPatternUser, DirectCast(User, UserData).NameTrue)
+ End Function
Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String
Try
Dim code$ = DirectCast(User, UserData).GetPostCodeById(Media.Post.ID)
diff --git a/SCrawler/API/Instagram/UserData.vb b/SCrawler/API/Instagram/UserData.vb
index 47026d0..e46f08a 100644
--- a/SCrawler/API/Instagram/UserData.vb
+++ b/SCrawler/API/Instagram/UserData.vb
@@ -30,7 +30,7 @@ Namespace API.Instagram
Private Const Name_NameTrue As String = "NameTrue"
#End Region
#Region "Declarations"
- Private Structure PostKV : Implements IEContainerProvider
+ Protected Structure PostKV : Implements IEContainerProvider
Private Const Name_Code As String = "Code"
Private Const Name_Section As String = "Section"
Friend Code As String
@@ -78,8 +78,8 @@ Namespace API.Instagram
Friend Property GetStories As Boolean
Friend Property GetStoriesUser As Boolean
Friend Property GetTaggedData As Boolean
- Private _NameTrue As String = String.Empty
- Private ReadOnly Property NameTrue As String
+ Protected _NameTrue As String = String.Empty
+ Friend ReadOnly Property NameTrue As String
Get
Return _NameTrue.IfNullOrEmpty(Name)
End Get
@@ -143,12 +143,22 @@ Namespace API.Instagram
Throw New ExitException
End Sub
End Class
- Private Sub LoadSavePostsKV(ByVal Load As Boolean)
+ Private ReadOnly Property MyFilePostsKV As SFile
+ Get
+ Dim f As SFile = MyFilePosts
+ If Not f.IsEmptyString Then
+ f.Name &= "_KV"
+ f.Extension = "xml"
+ Return f
+ Else
+ Return Nothing
+ End If
+ End Get
+ End Property
+ Protected Sub LoadSavePostsKV(ByVal Load As Boolean)
Dim x As XmlFile
- Dim f As SFile = MyFilePosts
+ Dim f As SFile = MyFilePostsKV
If Not f.IsEmptyString Then
- f.Name &= "_KV"
- f.Extension = "xml"
If Load Then
PostsKVIDs.Clear()
x = New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True}
@@ -182,10 +192,8 @@ Namespace API.Instagram
Friend Function GetPostCodeById(ByVal PostID As String) As String
Try
If Not PostID.IsEmptyString Then
- Dim f As SFile = MyFilePosts
+ Dim f As SFile = MyFilePostsKV
If Not f.IsEmptyString Then
- f.Name &= "_KV"
- f.Extension = "xml"
Dim l As List(Of PostKV) = Nothing
Using x As New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True}
x.LoadData()
@@ -213,11 +221,15 @@ Namespace API.Instagram
End If
End Function
Private _DownloadingInProgress As Boolean = False
+ Private _Limit As Integer = -1
+ Private _TotalPostsParsed As Integer = 0
Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken)
UserNameRequested = False
Dim s As Sections = Sections.Timeline
Dim errorFound As Boolean = False
Try
+ _Limit = If(DownloadTopCount, -1)
+ _TotalPostsParsed = 0
LoadSavePostsKV(True)
_DownloadingInProgress = True
AddHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived
@@ -270,7 +282,7 @@ Namespace API.Instagram
Catch ex As Exception
End Try
End Sub
- Private Sub UpdateResponser()
+ Protected Overridable Sub UpdateResponser()
Try
If _DownloadingInProgress AndAlso Not Responser Is Nothing AndAlso Not Responser.Disposed Then
_DownloadingInProgress = False
@@ -280,10 +292,10 @@ Namespace API.Instagram
Catch
End Try
End Sub
- Private Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As EventArguments.WebDataResponse)
+ Protected Overridable Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As EventArguments.WebDataResponse)
Declarations.UpdateResponser(e, Responser)
End Sub
- Private Enum Sections : Timeline : Tagged : Stories : UserStories : SavedPosts : End Enum
+ Protected Enum Sections : Timeline : Tagged : Stories : UserStories : SavedPosts : End Enum
Private Const StoriesFolder As String = "Stories"
Private Const TaggedFolder As String = "Tagged"
#Region "429 bypass"
@@ -572,6 +584,7 @@ Namespace API.Instagram
Dim URL$ = String.Empty
Dim dValue% = 1
Dim _Index% = 0
+ Dim before%
If PostsToReparse.Count > 0 Then ProgressPre.ChangeMax(PostsToReparse.Count)
Try
Do While dValue = 1
@@ -600,7 +613,12 @@ Namespace API.Instagram
If Not j Is Nothing Then
If If(j("items")?.Count, 0) > 0 Then
With j("items")
- For Each jj In .Self : ObtainMedia(jj, PostsToReparse(i).ID) : Next
+ For Each jj In .Self
+ before = _TempMediaList.Count
+ ObtainMedia(jj, PostsToReparse(i).ID)
+ If Not before = _TempMediaList.Count Then _TotalPostsParsed += 1
+ If _Limit > 0 And _TotalPostsParsed >= _Limit Then Throw New ExitException
+ Next
End With
End If
j.Dispose()
@@ -643,13 +661,17 @@ Namespace API.Instagram
End Using
End If
End Sub
- Private Function DefaultParser(ByVal Items As IEnumerable(Of EContainer), ByVal Section As Sections, ByVal Token As CancellationToken,
- Optional ByVal SpecFolder As String = Nothing) As Boolean
+ Protected DefaultParser_ElemNode() As Object = Nothing
+ Protected DefaultParser_IgnorePass As Boolean = False
+ Protected DefaultParser_PostUrlCreator As Func(Of PostKV, String) = Function(post) $"https://www.instagram.com/p/{post.Code}/"
+ Protected Function DefaultParser(ByVal Items As IEnumerable(Of EContainer), ByVal Section As Sections, ByVal Token As CancellationToken,
+ Optional ByVal SpecFolder As String = Nothing) As Boolean
ThrowAny(Token)
If Items.Count > 0 Then
Dim PostIDKV As PostKV
Dim Pinned As Boolean
- Dim PostDate$
+ Dim PostDate$, PostOriginUrl$
+ Dim before%
If SpecFolder.IsEmptyString Then
Select Case Section
Case Sections.Tagged : SpecFolder = TaggedFolder
@@ -660,22 +682,26 @@ Namespace API.Instagram
ProgressPre.ChangeMax(Items.Count)
For Each nn In Items
ProgressPre.Perform()
- With nn
+ With If(Not DefaultParser_ElemNode Is Nothing, nn.ItemF(DefaultParser_ElemNode), nn)
PostIDKV = New PostKV(.Value("code"), .Value("id"), Section)
+ PostOriginUrl = DefaultParser_PostUrlCreator(PostIDKV)
Pinned = .Contains("timeline_pinned_user_ids")
- If PostKvExists(PostIDKV) Then
+ If Not DefaultParser_IgnorePass AndAlso PostKvExists(PostIDKV) Then
If Not Pinned Then Return False
Else
_TempPostsList.Add(PostIDKV.ID)
PostsKVIDs.ListAddValue(PostIDKV, LNC)
PostDate = .Value("taken_at")
- If Not IsSavedPosts Then
+ If Not DefaultParser_IgnorePass And Not IsSavedPosts Then
Select Case CheckDatesLimit(PostDate, UnixDate32Provider)
Case DateResult.Skip : Continue For
Case DateResult.Exit : If Not Pinned Then Return False
End Select
End If
- ObtainMedia(.Self, PostIDKV.ID, SpecFolder, PostDate)
+ before = _TempMediaList.Count
+ ObtainMedia(.Self, PostIDKV.ID, SpecFolder, PostDate,, PostOriginUrl)
+ If Not before = _TempMediaList.Count Then _TotalPostsParsed += 1
+ If _Limit > 0 And _TotalPostsParsed >= _Limit Then Return False
End If
End With
Next
@@ -686,7 +712,7 @@ Namespace API.Instagram
End Function
#End Region
#Region "Code ID converters"
- Private Function CodeToID(ByVal Code As String) As String
+ Protected Function CodeToID(ByVal Code As String) As String
Const CodeSymbols$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
Try
If Not Code.IsEmptyString Then
@@ -706,12 +732,19 @@ Namespace API.Instagram
End Function
#End Region
#Region "Obtain Media"
- Private Sub ObtainMedia(ByVal n As EContainer, ByVal PostID As String, Optional ByVal SpecialFolder As String = Nothing,
- Optional ByVal DateObj As String = Nothing)
+ Protected ObtainMedia_SizeFuncVid As Func(Of EContainer, Sizes) = Nothing
+ Protected ObtainMedia_SizeFuncPic As Func(Of EContainer, Sizes) = Nothing
+ Protected ObtainMedia_AllowAbstract As Boolean = False
+ Protected Sub ObtainMedia(ByVal n As EContainer, ByVal PostID As String, Optional ByVal SpecialFolder As String = Nothing,
+ Optional ByVal DateObj As String = Nothing, Optional ByVal InitialType As Integer = -1,
+ Optional ByVal PostOriginUrl As String = Nothing)
Try
+ Dim wrongData As Predicate(Of Sizes) = Function(_ss) _ss.HasError Or _ss.Data.IsEmptyString
Dim img As Predicate(Of EContainer) = Function(_img) Not _img.Name.IsEmptyString AndAlso _img.Name.StartsWith("image_versions") AndAlso _img.Count > 0
Dim vid As Predicate(Of EContainer) = Function(_vid) Not _vid.Name.IsEmptyString AndAlso _vid.Name.StartsWith("video_versions") AndAlso _vid.Count > 0
Dim ss As Func(Of EContainer, Sizes) = Function(_ss) New Sizes(_ss.Value("width"), _ss.Value("url"))
+ Dim ssVid As Func(Of EContainer, Sizes) = ss
+ Dim ssPic As Func(Of EContainer, Sizes) = ss
Dim mDate As Func(Of EContainer, String) = Function(ByVal elem As EContainer) As String
If Not DateObj.IsEmptyString Then Return DateObj
If elem.Contains("taken_at") Then
@@ -731,28 +764,41 @@ Namespace API.Instagram
End If
End If
End Function
+ If Not ObtainMedia_SizeFuncVid Is Nothing Then ssVid = ObtainMedia_SizeFuncVid
+ If Not ObtainMedia_SizeFuncPic Is Nothing Then ssPic = ObtainMedia_SizeFuncPic
If n.Count > 0 Then
Dim l As New List(Of Sizes)
Dim d As EContainer
Dim t%
+ Dim abstractDecision As Boolean = False
'8 - gallery
'2 - one video
'1 - one picture
t = n.Value("media_type").FromXML(Of Integer)(-1)
+ If t = -1 And InitialType = 8 And ObtainMedia_AllowAbstract Then
+ If n.Contains(vid) Then
+ t = 2
+ abstractDecision = True
+ ElseIf n.Contains(img) Then
+ t = 1
+ abstractDecision = True
+ End If
+ End If
If t >= 0 Then
Select Case t
Case 1
If n.Contains(img) Then
- t = n.Value("media_type").FromXML(Of Integer)(-1)
+ If Not abstractDecision Then t = n.Value("media_type").FromXML(Of Integer)(-1)
DateObj = mDate(n)
If t >= 0 Then
With n.ItemF({img, "candidates"}).XmlIfNothing
If .Count > 0 Then
l.Clear()
- l.ListAddList(.Select(ss), LNC)
+ l.ListAddList(.Select(ssPic), LNC)
+ If l.Count > 0 Then l.RemoveAll(wrongData)
If l.Count > 0 Then
l.Sort()
- _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, l.First.Data, PostID, DateObj, SpecialFolder), LNC)
+ _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, l.First.Data, PostID, DateObj, SpecialFolder, PostOriginUrl), LNC)
l.Clear()
End If
End If
@@ -765,10 +811,11 @@ Namespace API.Instagram
With n.ItemF({vid}).XmlIfNothing
If .Count > 0 Then
l.Clear()
- l.ListAddList(.Select(ss), LNC)
+ l.ListAddList(.Select(ssVid), LNC)
+ If l.Count > 0 Then l.RemoveAll(wrongData)
If l.Count > 0 Then
l.Sort()
- _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, l.First.Data, PostID, DateObj, SpecialFolder), LNC)
+ _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, l.First.Data, PostID, DateObj, SpecialFolder, PostOriginUrl), LNC)
l.Clear()
End If
End If
@@ -778,7 +825,7 @@ Namespace API.Instagram
DateObj = mDate(n)
With n("carousel_media").XmlIfNothing
If .Count > 0 Then
- For Each d In .Self : ObtainMedia(d, PostID, SpecialFolder, DateObj) : Next
+ For Each d In .Self : ObtainMedia(d, PostID, SpecialFolder, DateObj, 8, PostOriginUrl) : Next
End If
End With
End Select
@@ -939,6 +986,12 @@ Namespace API.Instagram
DownloadContentDefault(Token)
End Sub
#End Region
+#Region "Erase"
+ Protected Overrides Sub EraseData_AdditionalDataFiles()
+ Dim f As SFile = MyFilePostsKV
+ If f.Exists Then f.Delete(SFO.File, SFODelete.DeleteToRecycleBin, EDP.ReturnValue)
+ End Sub
+#End Region
#Region "Exceptions"
'''
'''
@@ -996,9 +1049,9 @@ Namespace API.Instagram
#End Region
#Region "Create media"
Private Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String,
- Optional ByVal SpecialFolder As String = Nothing) As UserMedia
+ Optional ByVal SpecialFolder As String = Nothing, Optional ByVal PostOriginUrl As String = Nothing) As UserMedia
_URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern))
- Dim m As New UserMedia(_URL, t) With {.Post = New UserPost With {.ID = PostID}}
+ Dim m As New UserMedia(_URL, t) With {.URL_BASE = PostOriginUrl.IfNullOrEmpty(_URL), .Post = New UserPost With {.ID = PostID}}
If Not m.URL.IsEmptyString Then m.File = CStr(RegexReplace(m.URL, FilesPattern))
If Not PostDate.IsEmptyString Then m.Post.Date = AConvert(Of Date)(PostDate, UnixDate32Provider, Nothing) Else m.Post.Date = Nothing
m.SpecialFolder = SpecialFolder
diff --git a/SCrawler/API/ThreadsNet/SiteSettings.vb b/SCrawler/API/ThreadsNet/SiteSettings.vb
new file mode 100644
index 0000000..cd04063
--- /dev/null
+++ b/SCrawler/API/ThreadsNet/SiteSettings.vb
@@ -0,0 +1,166 @@
+' Copyright (C) 2023 Andy https://github.com/AAndyProgram
+' This program is free software: you can redistribute it and/or modify
+' it under the terms of the GNU General Public License as published by
+' the Free Software Foundation, either version 3 of the License, or
+' (at your option) any later version.
+'
+' This program is distributed in the hope that it will be useful,
+' but WITHOUT ANY WARRANTY
+Imports SCrawler.API.Base
+Imports SCrawler.Plugin
+Imports SCrawler.Plugin.Attributes
+Imports PersonalUtilities.Tools.Web.Clients
+Imports PersonalUtilities.Tools.Web.Cookies
+Imports PersonalUtilities.Functions.RegularExpressions
+Imports IG = SCrawler.API.Instagram.SiteSettings
+Namespace API.ThreadsNet
+
+ Friend Class SiteSettings : Inherits SiteSettingsBase
+#Region "Declarations"
+ Friend Overrides ReadOnly Property Icon As Icon
+ Get
+ Return My.Resources.SiteResources.ThreadsIcon_192
+ End Get
+ End Property
+ Private ReadOnly _Image As Image
+ Friend Overrides ReadOnly Property Image As Image
+ Get
+ Return _Image
+ End Get
+ End Property
+#Region "Authorization"
+
+ Friend ReadOnly Property HH_CSRF_TOKEN As PropertyValue
+
+ Friend Property HH_IG_APP_ID As PropertyValue
+
+ Friend Property HH_ASBD_ID As PropertyValue
+
+ Private Property HH_BROWSER As PropertyValue
+
+ Private Property HH_BROWSER_EXT As PropertyValue
+
+ Private Property HH_PLATFORM As PropertyValue
+
+ Private ReadOnly Property HH_USER_AGENT As PropertyValue
+ Private Sub ChangeResponserFields(ByVal PropName As String, ByVal Value As Object)
+ If Not PropName.IsEmptyString Then
+ Dim f$ = String.Empty
+ Dim isUserAgent As Boolean = False
+ Select Case PropName
+ Case NameOf(HH_IG_APP_ID) : f = IG.Header_IG_APP_ID
+ Case NameOf(HH_ASBD_ID) : f = IG.Header_ASBD_ID
+ Case NameOf(HH_CSRF_TOKEN) : f = IG.Header_CSRF_TOKEN
+ Case NameOf(HH_BROWSER) : f = IG.Header_Browser
+ Case NameOf(HH_BROWSER_EXT) : f = IG.Header_BrowserExt
+ Case NameOf(HH_PLATFORM) : f = IG.Header_Platform
+ Case NameOf(HH_USER_AGENT) : isUserAgent = True
+ End Select
+ If Not f.IsEmptyString Then
+ Responser.Headers.Remove(f)
+ If Not CStr(Value).IsEmptyString Then Responser.Headers.Add(f, CStr(Value))
+ ElseIf isUserAgent Then
+ Responser.UserAgent = CStr(Value)
+ End If
+ End If
+ End Sub
+#End Region
+#End Region
+#Region "Initializer"
+ Friend Sub New()
+ MyBase.New("Threads", "threads.net")
+ _AllowUserAgentUpdate = False
+ _Image = My.Resources.SiteResources.ThreadsIcon_192.ToBitmap
+
+ Dim app_id$ = String.Empty
+ Dim token$ = String.Empty
+ Dim asbd$ = String.Empty
+ Dim browser$ = String.Empty
+ Dim browserExt$ = String.Empty
+ Dim platform$ = String.Empty
+ Dim useragent$ = String.Empty
+
+ With Responser
+ .Accept = "*/*"
+ 'URGENT: remove after debug
+ .DeclaredError = EDP.SendToLog + EDP.ThrowException
+ If .UserAgentExists Then useragent = .UserAgent
+ With .Headers
+ If .Count > 0 Then
+ token = .Value(IG.Header_CSRF_TOKEN)
+ app_id = .Value(IG.Header_IG_APP_ID)
+ asbd = .Value(IG.Header_ASBD_ID)
+ browser = .Value(IG.Header_Browser)
+ browserExt = .Value(IG.Header_BrowserExt)
+ platform = .Value(IG.Header_Platform)
+ End If
+ .Add("Authority", "www.threads.net")
+ .Add("Origin", "https://www.threads.net")
+ .Add("Upgrade-Insecure-Requests", 1)
+ .Add("Sec-Ch-Ua-Model", "")
+ .Add("Sec-Ch-Ua-Mobile", "?0")
+ .Add("Sec-Ch-Ua-Platform", """Windows""")
+ .Add("Sec-Fetch-Dest", "empty")
+ .Add("Sec-Fetch-Mode", "cors")
+ .Add("Sec-Fetch-Site", "same-origin")
+ .Add("Sec-Fetch-User", "?1")
+ .Add("x-fb-friendly-name", "BarcelonaProfileThreadsTabRefetchableQuery")
+ End With
+ .CookiesExtractMode = Responser.CookiesExtractModes.Any
+ .CookiesUpdateMode = CookieKeeper.UpdateModes.ReplaceByNameAll
+ .CookiesExtractedAutoSave = False
+ .Cookies.ChangedAllowInternalDrop = False
+ .Cookies.Changed = False
+ End With
+
+ HH_CSRF_TOKEN = New PropertyValue(token, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_CSRF_TOKEN), v))
+ HH_IG_APP_ID = New PropertyValue(app_id, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_IG_APP_ID), v))
+ HH_ASBD_ID = New PropertyValue(asbd, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_ASBD_ID), v))
+ HH_BROWSER = New PropertyValue(browser, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_BROWSER), v))
+ HH_BROWSER_EXT = New PropertyValue(browserExt, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_BROWSER_EXT), v))
+ HH_PLATFORM = New PropertyValue(platform, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_PLATFORM), v))
+ HH_USER_AGENT = New PropertyValue(useragent, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_USER_AGENT), v))
+
+ UrlPatternUser = "https://www.threads.net/@{0}"
+ UserRegex = RParams.DMS("threads.net/@([^/\?&]+)", 1)
+ ImageVideoContains = "threads.net"
+ End Sub
+#End Region
+#Region "UpdateResponserData"
+ Friend Sub UpdateResponserData(ByVal Resp As Responser)
+ With Responser.Cookies
+ Dim csrf$ = String.Empty
+ .Update(Resp.Cookies)
+ If .Changed Then
+ Responser.SaveCookies()
+ .Changed = False
+ csrf = If(.FirstOrDefault(Function(c) c.Name.StringToLower = IG.Header_CSRF_TOKEN_COOKIE)?.Value, String.Empty)
+ End If
+ If Not csrf.IsEmptyString AndAlso Not AEquals(Of String)(csrf, HH_CSRF_TOKEN.Value) Then HH_CSRF_TOKEN.Value = csrf
+ End With
+ End Sub
+#End Region
+#Region "GetInstance"
+ Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider
+ Return New UserData
+ End Function
+#End Region
+#Region "BaseAuthExists, GetUserUrl, GetUserPostUrl"
+ Friend Overrides Function BaseAuthExists() As Boolean
+ Return Responser.CookiesExists And {HH_CSRF_TOKEN, HH_IG_APP_ID}.All(Function(v) ACheck(Of String)(v.Value))
+ End Function
+ Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String
+ Return String.Format(UrlPatternUser, DirectCast(User, UserData).NameTrue)
+ End Function
+ Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String
+ Try
+ Dim code$ = DirectCast(User, UserData).GetPostCodeById(Media.Post.ID)
+ Dim name$ = DirectCast(User, UserData).NameTrue
+ If Not code.IsEmptyString Then Return $"https://www.threads.net/@{name}/post/{code}/" Else Return String.Empty
+ Catch ex As Exception
+ Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "Can't open user's post", String.Empty)
+ End Try
+ End Function
+#End Region
+ End Class
+End Namespace
\ No newline at end of file
diff --git a/SCrawler/API/ThreadsNet/UserData.vb b/SCrawler/API/ThreadsNet/UserData.vb
new file mode 100644
index 0000000..8562aa6
--- /dev/null
+++ b/SCrawler/API/ThreadsNet/UserData.vb
@@ -0,0 +1,290 @@
+' Copyright (C) 2023 Andy https://github.com/AAndyProgram
+' This program is free software: you can redistribute it and/or modify
+' it under the terms of the GNU General Public License as published by
+' the Free Software Foundation, either version 3 of the License, or
+' (at your option) any later version.
+'
+' This program is distributed in the hope that it will be useful,
+' but WITHOUT ANY WARRANTY
+Imports System.Threading
+Imports SCrawler.API.Base
+Imports SCrawler.API.YouTube.Objects
+Imports PersonalUtilities.Functions.XML
+Imports PersonalUtilities.Functions.RegularExpressions
+Imports PersonalUtilities.Tools.Web.Documents.JSON
+Imports PersonalUtilities.Tools.Web.Clients
+Imports PersonalUtilities.Tools.Web.Clients.EventArguments
+Imports IGS = SCrawler.API.Instagram.SiteSettings
+Namespace API.ThreadsNet
+ Friend Class UserData : Inherits Instagram.UserData
+#Region "Declarations"
+ Private Const Header_FB_LSD As String = "x-fb-lsd"
+ Private ReadOnly Property MySettings As SiteSettings
+ Get
+ Return HOST.Source
+ End Get
+ End Property
+ Private ReadOnly ObtainMedia_SizeFuncPic_RegexP As RParams = RParams.DMS("_p(\d+)x(\d+)", 1, EDP.ReturnValue)
+ Private ReadOnly ObtainMedia_SizeFuncPic_RegexS As RParams = RParams.DMS("_s(\d+)x(\d+)", 1, EDP.ReturnValue)
+ Private ReadOnly DefaultParser_ElemNode_Default() As Object = {"node", "thread_items", 0, "post"}
+ Private OPT_LSD As String = String.Empty
+ Private OPT_FB_DTSG As String = String.Empty
+ Private ReadOnly Property Valid As Boolean
+ Get
+ Return Not OPT_LSD.IsEmptyString And Not OPT_FB_DTSG.IsEmptyString And Not ID.IsEmptyString
+ End Get
+ End Property
+#End Region
+#Region "Loader"
+ Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean)
+ End Sub
+#End Region
+#Region "Exchange"
+ Friend Overrides Function ExchangeOptionsGet() As Object
+ Return Nothing
+ End Function
+ Friend Overrides Sub ExchangeOptionsSet(ByVal Obj As Object)
+ End Sub
+#End Region
+#Region "Initializer"
+ Friend Sub New()
+ ObtainMedia_SizeFuncPic = Function(ByVal ss As EContainer) As Sizes
+ If ss.Value("url").IsEmptyString Then
+ Return New Sizes("----", "")
+ ElseIf Not ss.Value("width").IsEmptyString Then
+ Return New Sizes(ss.Value("height").IfNullOrEmpty(ss.Value("width")), ss.Value("url"))
+ Else
+ Dim rval$ = RegexReplace(ss.Value("url"), ObtainMedia_SizeFuncPic_RegexP)
+ If Not rval.IsEmptyString Then Return New Sizes(rval, ss.Value("url"))
+ rval = RegexReplace(ss.Value("url"), ObtainMedia_SizeFuncPic_RegexS)
+ If Not rval.IsEmptyString Then Return New Sizes(AConvert(Of Integer)(rval, 1) * -1, ss.Value("url"))
+ Return New Sizes(10000, ss.Value("url"))
+ End If
+ End Function
+ ObtainMedia_SizeFuncVid = Function(ss) If(ss.Value("url").IsEmptyString, New Sizes("----", ""), New Sizes(10000, ss.Value("url")))
+ ObtainMedia_AllowAbstract = True
+ DefaultParser_ElemNode = DefaultParser_ElemNode_Default
+ DefaultParser_PostUrlCreator = Function(post) $"https://www.threads.net/@{NameTrue}/post/{post.Code}"
+ End Sub
+#End Region
+#Region "Download functions"
+ Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken)
+ Dim errorFound As Boolean = False
+ Try
+ Responser.Method = "POST"
+ AddHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived
+ LoadSavePostsKV(True)
+ OPT_LSD = String.Empty
+ OPT_FB_DTSG = String.Empty
+ DownloadData(String.Empty, Token)
+ Catch ex As Exception
+ errorFound = True
+ Throw ex
+ Finally
+ Responser.Method = "POST"
+ UpdateResponser()
+ MySettings.UpdateResponserData(Responser)
+ If Not errorFound Then LoadSavePostsKV(False)
+ End Try
+ End Sub
+ Protected Overrides Sub UpdateResponser()
+ If Not Responser Is Nothing AndAlso Not Responser.Disposed Then
+ RemoveHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived
+ End If
+ End Sub
+ Protected Overrides Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As WebDataResponse)
+ If e.CookiesExists Then
+ Dim csrf$ = If(e.Cookies.FirstOrDefault(Function(v) v.Name.StringToLower = IGS.Header_CSRF_TOKEN_COOKIE)?.Value, String.Empty)
+ If Not csrf.IsEmptyString AndAlso Not AEquals(Of String)(csrf, Responser.Headers.Value(IGS.Header_CSRF_TOKEN)) Then _
+ Responser.Headers.Add(IGS.Header_CSRF_TOKEN, csrf)
+ End If
+ End Sub
+ Private Overloads Sub DownloadData(ByVal Cursor As String, ByVal Token As CancellationToken)
+ Const urlPattern$ = "https://www.threads.net/api/graphql?lsd={0}&variables={1}&doc_id=6371597506283707&fb_api_req_friendly_name=BarcelonaProfileThreadsTabRefetchableQuery&server_timestamps=true&fb_dtsg={2}"
+ Const var_init$ = """userID"":""{0}"""
+ Const var_cursor$ = """after"":""{1}"",""before"":null,""first"":25,""last"":null,""userID"":""{0}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false"
+ Dim URL$ = String.Empty
+ Try
+ If Not Valid Then
+ Dim idIsNull As Boolean = ID.IsEmptyString
+ UpdateCredentials()
+ If idIsNull And Not ID.IsEmptyString Then _ForceSaveUserInfo = True
+ End If
+ If Not Valid Then Throw New Plugin.ExitException("Some credentials are missing")
+
+ Responser.Method = "POST"
+ Responser.Referer = $"https://www.threads.net/@{NameTrue}"
+ Responser.Headers.Add(Header_FB_LSD, OPT_LSD)
+
+ Dim nextCursor$ = String.Empty
+ Dim dataFound As Boolean = False
+
+ Dim vars$
+ If Cursor.IsEmptyString Then
+ vars = String.Format(var_init, ID)
+ Else
+ vars = String.Format(var_cursor, ID, Cursor)
+ End If
+ vars = SymbolsConverter.ASCII.EncodeSymbolsOnly("{" & vars & "}")
+
+ URL = String.Format(urlPattern, OPT_LSD, vars, SymbolsConverter.ASCII.EncodeSymbolsOnly(OPT_FB_DTSG))
+
+ Using j As EContainer = GetDocument(URL, Token)
+ If j.ListExists Then
+ With j({"data", "mediaData"})
+ If .ListExists Then
+ nextCursor = .Value({"page_info"}, "end_cursor")
+ With .Item({"edges"})
+ If .ListExists Then dataFound = DefaultParser(.Self, Sections.Timeline, Token)
+ End With
+ End If
+ End With
+ End If
+ End Using
+
+ If dataFound And Not nextCursor.IsEmptyString Then DownloadData(nextCursor, Token)
+ Catch ex As Exception
+ ProcessException(ex, Token, $"data downloading error [{URL}]")
+ End Try
+ End Sub
+ Private Function GetDocument(ByVal URL As String, ByVal Token As CancellationToken, Optional ByVal Round As Integer = 0) As EContainer
+ Try
+ ThrowAny(Token)
+ If Round > 0 AndAlso Not UpdateCredentials() Then Throw New Exception("Failed to update credentials")
+ ThrowAny(Token)
+ Dim r$ = Responser.GetResponse(URL)
+ If Not r.IsEmptyString Then Return JsonDocument.Parse(r) Else Throw New Exception("Failed to get a response")
+ Catch ex As Exception
+ If Round = 0 Then
+ Return GetDocument(URL, Token, Round + 1)
+ Else
+ Throw ex
+ End If
+ End Try
+ End Function
+ Private Function UpdateCredentials(Optional ByVal e As ErrorsDescriber = Nothing) As Boolean
+ Dim URL$ = $"https://www.threads.net/@{NameTrue}"
+ OPT_LSD = String.Empty
+ OPT_FB_DTSG = String.Empty
+ Try
+ Responser.Method = "GET"
+ Responser.Referer = URL
+ Responser.Headers.Remove(Header_FB_LSD)
+ Dim r$ = Responser.GetResponse(URL,, EDP.SendToLog + EDP.ThrowException)
+ Dim rr As RParams
+ Dim tt$, ttVal$
+ If Not r.IsEmptyString Then
+ rr = RParams.DM("\[\],{""token"":""(.*?)""},\d+\]", 0, RegexReturn.List, EDP.ReturnValue)
+ Dim tokens As List(Of String) = RegexReplace(r, rr)
+ If tokens.ListExists Then
+ With rr
+ .Match = Nothing
+ .MatchSub = 1
+ .WhatGet = RegexReturn.Value
+ End With
+ For Each tt In tokens
+ If Not OPT_FB_DTSG.IsEmptyString And Not OPT_LSD.IsEmptyString Then
+ Exit For
+ Else
+ ttVal = RegexReplace(tt, rr)
+ If Not ttVal.IsEmptyString Then
+ If ttVal.Contains(":") Then
+ If OPT_FB_DTSG.IsEmptyString Then OPT_FB_DTSG = ttVal
+ Else
+ If OPT_LSD.IsEmptyString Then OPT_LSD = ttVal
+ End If
+ End If
+ End If
+ Next
+ End If
+ If ID.IsEmptyString Then ID = RegexReplace(r, RParams.DMS("""props"":\{""user_id"":""(\d+)""\},", 1, EDP.ReturnValue))
+ End If
+ Return Valid
+ Catch ex As Exception
+ Dim notFound$ = String.Empty
+ If OPT_FB_DTSG.IsEmptyString Then notFound.StringAppend(Header_FB_LSD)
+ If OPT_LSD.IsEmptyString Then notFound.StringAppend("lsd")
+ If ID.IsEmptyString Then notFound.StringAppend("User ID")
+ LogError(ex, $"failed to update some{IIf(notFound.IsEmptyString, String.Empty, $" ({notFound})")} credentials", e)
+ Return False
+ End Try
+ End Function
+#End Region
+#Region "ReparseMissing"
+ Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken)
+ Const varsPattern$ = """postID"":""{0}"",""userID"":""{1}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false"
+ 'Const varsPattern$ = "{""postID"":""{0}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false}"
+ Const urlPattern$ = "https://www.threads.net/api/graphql?lsd={0}&variables={1}&fb_api_req_friendly_name=BarcelonaPostPageQuery&server_timestamps=true&fb_dtsg={2}&doc_id=25460088156920903"
+ Dim rList As New List(Of Integer)
+ Dim URL$ = String.Empty
+ DefaultParser_ElemNode = Nothing
+ DefaultParser_IgnorePass = True
+ Try
+ If ContentMissingExists Then
+ Responser.Method = "POST"
+ Responser.Referer = $"https://www.threads.net/@{NameTrue}"
+ If Not IsSingleObjectDownload AndAlso Not UpdateCredentials() Then Throw New Exception("Failed to update credentials")
+ Dim m As UserMedia
+ Dim vars$
+ Dim j As EContainer
+ ProgressPre.ChangeMax(_ContentList.Count)
+ For i% = 0 To _ContentList.Count - 1
+ ProgressPre.Perform()
+ m = _ContentList(i)
+ If m.State = UserMedia.States.Missing And Not m.Post.ID.IsEmptyString Then
+ ThrowAny(Token)
+ vars = SymbolsConverter.ASCII.EncodeSymbolsOnly("{" & String.Format(varsPattern, m.Post.ID.Split("_").FirstOrDefault, ID) & "}")
+ URL = String.Format(urlPattern, OPT_LSD, vars, SymbolsConverter.ASCII.EncodeSymbolsOnly(OPT_FB_DTSG))
+
+ j = GetDocument(URL, Token)
+ If j.ListExists Then
+ With j.ItemF({"data", "data", "edges", 0, "node", "thread_items", 0, "post"})
+ If .ListExists AndAlso DefaultParser({ .Self}, Sections.Timeline, Token) Then rList.Add(i)
+ End With
+ j.Dispose()
+ End If
+ End If
+ Next
+ End If
+ Catch ex As Exception
+ ProcessException(ex, Token, $"ReparseMissing error [{URL}]")
+ Finally
+ DefaultParser_ElemNode = DefaultParser_ElemNode_Default
+ DefaultParser_IgnorePass = False
+ If rList.Count > 0 Then
+ For i% = rList.Count - 1 To 0 Step -1 : _ContentList.RemoveAt(rList(i)) : Next
+ rList.Clear()
+ End If
+ End Try
+ End Sub
+#End Region
+#Region "DownloadSingleObject"
+ Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken)
+ Dim url$ = Data.URL_BASE.IfNullOrEmpty(Data.URL)
+ Dim postCode$ = RegexReplace(url, RParams.DMS("post/([^/\?&]+)", 1, EDP.ReturnValue))
+ If Not postCode.IsEmptyString Then
+ Dim postId$ = CodeToID(postCode)
+ If Not postId.IsEmptyString Then
+ _NameTrue = MySettings.IsMyUser(url).UserName
+ DefaultParser_PostUrlCreator = Function(post) url
+ If Not _NameTrue.IsEmptyString AndAlso UpdateCredentials(EDP.ReturnValue) Then
+ _ContentList.Add(New UserMedia(url) With {.State = UserMedia.States.Missing, .Post = postId})
+ ReparseMissing(Token)
+ End If
+ End If
+ End If
+ End Sub
+#End Region
+#Region "ThrowAny"
+ Friend Overrides Sub ThrowAny(ByVal Token As CancellationToken)
+ ThrowAnyImpl(Token)
+ End Sub
+#End Region
+#Region "DownloadingException"
+ Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False,
+ Optional ByVal EObj As Object = Nothing) As Integer
+ Return 0
+ End Function
+#End Region
+ End Class
+End Namespace
\ No newline at end of file
diff --git a/SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico b/SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico
new file mode 100644
index 0000000000000000000000000000000000000000..bc454b49c9025723a8cc9c33fe1e41050fa9a169
GIT binary patch
literal 169662
zcmeI53%FH9^}vr$G$nkMj|lNCQexqgAk;ue{j?CYP?MreGDT1@|7P4zBsCDBK*=x(
z4GlE}Oi@8DDLzR>QB+K?2`Neff{6H-|NQ1M_uF^hz0cV*XYYN^-e=bL&E5CxJ$v?=
zHEU+pTC-+Np|C|^o5D^z738&j;k>O2h0_a#!XA4x{=R1WLg8U~uYLQ*-*?D-Cw#F`
zIP9>^zYi!B-fveZ)YmuuKDf`8g&TSl3J1$FWL8;X<9&H46l6I8u$8>WCl|}~iAQ}!izWBv2iY>O-!e1`$P{woKcX!%pCvoA07m5cS
zctAY)BT=F=NII@#Qan*;_`_yOeSM`R9v=AAVT0ZQE9Cv&}Z5Lx&EkjP1AIUhJ~VF5=#M
z?-dtabdmS{rvG=`aYu#o4}bVW(W+G|F?Q@&@ys*Nh((JQiD}cOi7$QWOJc~7Aqqc@
zulIW3fd`7mAAelzv(G-_=%bHT?|tJN-%#|s=bn4SWtUwhCQO(huD$l!rf+)R=beB4
z^Pghpop%;r``Xvkw+9UxB#t`jC^2r_IPvbg@2c{liT6GIzkYw$U3XP&b=qmCiGTd#
zAF9mPUVBZI*Qrydz;`J_o!U=->ha7T<`1{}gE=G+SrOKuJt+(E~
zMLAyjZM*HZ;`hJ*z4+I^{w1Dx;t7SvzWeU0+T-rK?^ZO?czdtj_M;6BJM1t;OZZ}k
z9d;0Z`qQ7ppa1-4)wZ7Zns}fX|J-@!onr3Xxq|xn-VNm62OoT}8UKzPIWh=u-aSJm
z+y2A5x)0j+Us;4bFwV%Ks&JT0j9&`mm{kN0m(OmMi8_BKlb4=R{^y;QGTkNx?7C-q~EHrb%F4#?q5cI!WAo^UbPH
zG&D4b|NQ4a;>8zVRAbt}fdd8KD)Jpo?-qF&>;CeWzle`M`bex?xl-{sedMN_Zc=40
zTeeJW+O$dC4;wq=ci)z&MO%UsWA29^ekiWL{(9Aa>6aQ$#)-Z5+Di-_I#j&(-g~OQ
zF#hT1o@Q~L^Ogm7v=Y4VJnP-ai>(`5GuDPb!eQzG`|Ip>-
zmtPk2W9BcOF$bJ?-g)A0fBT!N*KdCF8?oPh`>AKl5p(9u5zjvRtQv#8Wd;4;r%xY6
z^K;HQN4?MZM>~A{@yFud|Nggl>7|#%h7B9UQ%^l54m#){mFI>VZV+$2`KFqmg39#X
zW4wi?jKAFf&Ue0}>d(ByJW5@D^rIh%wQJXk@#Dt}@W225`^9t5Jy#Nc=5ppVaNKRT
z-BkJg`t?(||KJBdP`~*mywBLbbm>xY#T8dXra$j7_b~rM7s@*L;Df~*Z@i&+o^k%W
z-~F!Y6SHT}7QiQ;e4=<1dBaPGp#Ou)C*SwK|9$cN^Uo`ug0D!Qe)_4Z7xNK35mcW4
zUJ!lA!~NZN-(B?V*;AZ#)>#5sVZnk0g8m!@Z|`^f?aQ1=d;avNKUMt7@9y2Zi?`l-
zOWbwWU4q~A3*;Z#+FMr8|9<|Y4>0$_pNkhSR9|9X7j>K%un#dh0m*Gwjb
zR*b29H)JerF$R$j*(_{)L(XA*W6Yq9;oq)Zy9VJ-*)3!M>Q!ufo;h=-KsJN-k&~!H
z2rU?&nF9i!`{x(N82CodFF*U)&lKN=y}!Baq~}NZ88sKjIlsTQxlGE|py%RP<&%0T
zrqxLaDQ=)l%0>=rFVnFyHOXN+$sff5(QSxKkIA%Frq5*x26Y3Fzvj)Gr{v3%
zPd-`c(uW^@xH$IMW5thu{9`d?$`tX=JMXBx$jRuuCF)kRk0F;m^w2}9yjNa%MfC6A
zzbI{yanak+O9AB7A~HI-h4ycF>8h))Qsun*>Z{_IV~#0Rm!{{)F3<@;M+wsJOVCBh
zsm#B~T*yY={O~{RLK`5XAroksLFYrhg*TQjU*04y>t|7}@Hv3I-@0|{rted?8*jW(
z{P(~A6`y_fnSk$*naKwq=(4d}A+N%x=xX|Yl&jVq`2H_`@r$N+sUxx*ux{NtWrM*+
zr2EC|ufN{ZPRKLh1e|fk8BJwG`TpK}@2%vMHEY%g_*=ho;)y4!_E@oEh3MP2uhI*U
z$Fb|&ci(;LTi8XAr{LWsOO~kow1qAs$`u}_Z=)x8ZG6akz#Vto(L$fYuC;2_Dy6f2
z{p(*B*t(!SG7IBml<(?1@D*+Q$Rm#^JCVMRoC#ou^6RD8lj`g16+fWk(k7IFtf}9R
zdZoPp^hd9L%r~Ja0B>D>`Q=KFMaP0qUU=aJ^^8BrfRiUr7Vu%z@9KPE-{(2wJ2chf
zZPN!m{~+65a>*si_Myu(xrWhSmx1jM+bMj&I5S_$=;(*KA7C%md2Oy?{6|@gi&|e|
zyrrMP@8HBZiQUAM-~Bj-^*_FYeSrDxxZ{pfcrdooN6<%sr=NaW*_J62{f=k!Q4`r9
zZ2UpL(Q^xZ4ccQzhac!`j7iwpb$zgLGX{~5KB@0Vx$5y3eb77pV!y!N!q}t%UT3U8
z*Y)ak*dVY~p%eN0YbhL9CkUH=khK_ZXp5i!{O977Q%+I(x|hbtWQ+#@vWxdwsc-X~
z{I>Hi^Tfo76VecFH+C?;EEIx+mcRFu&*z8a@#Cw0YyAUT1oDu!AsO;G-_*Jwy3ng8
zIFy+en*rbS0>A5Js)YWj@zDAhJXusXkk13#Cg8sAW9Z)(%S3xOK|`U?s=iRzstz+j
zO2D`3ORy07SCOgIc@<%iP=Y3_mK&Ja-dP}B_GDW3Lx%v3ka+&7HbdgNp
z5|xfSZ+>Tqye7$WQR@f};4(y}U1TZ-2g--{$W#q@`|AfzGi5qRrXb+{qcZv5tG3_Z
z=&u7j1Kq0)*D!p*6`VD|Tc64lmOt`+=2qk|WK2B|v0i|`19Ea4-`I>RI0N+U7i6+7
z8@U=E4s8B1&*w@!9UnTuzwwY{PAk72YVwp{`%LyRyJB>
zVQ*Vur($g!oi*&6A@_~vRcvAKwFdMsbP04aFTSmZnCLT7V*sx*ZCqMa#=+dQ2sNIaQXX@0cx_EJm^ldEFk4);r+=R~$KtIFA
z8}vSS<3sp{T)(DYYhbKljKS`4`st?!&cm!>(5?VBOdI=T5Ki2?_~MH-D#{;$H8Ou*
z>d1FX0k$msdHiD~?ZdhPfL+Pdw(Q48|Au$)jnROApnj&W)Cb#w2Iz>*9~&EXIO+xc
zH88JX6VvZ=WgSQ7i4%YP8kp+=c!#wD?|1nhp3t@u=HuV~_P5GzijAA`9A2RRwp#o$CAWyaxKYpEq=Ql!ea(fINY}2hXS@
zzH}OHz4g}E#x`t_x-a4*=BEqe#BI0Tra!Ixhc&+bD|Q^#ay3k!K3(CCJgRBLxBTmJ
z{&Gxy!>3wSVJ(UN;D2A+xWNnCe;ECF4|-@`Lw5V+FMp}VD_tjiqtPAw-?07d9ea>B
zgT`fKGu=k;e_dUjKxYZciyajkCO|(&?$f|JM})Oyqq;HAYGBPa$d<=^k1byV{gD2N
z-IehQKQCm@F=NIk9fonh53HLq2LzRGOMBjnf~xPw;|{?YWo
z|8vr$Nlk0erRqXEunrkCAL=s8y+-N3-ftkwAm8Il##r;$zy4ME6S97c{ED2T`3IiS
zKCj*~;{3nV_*v>(^kL*wEvvE)rsW)D|0HP-Zc*khT?cex_&8|XArEs0YtI_~_{Tpc
z!t)v*uE-DAKLFaDbqw!w{zp#0ZyPy-F_85g=&Ji8v~Sh)&*ZGN)`ifw?gnzq-^%DH;SApA;#
zcow}Cxjzb^@9z`fzmIR0)-#Y%gWAWY9#Qod)(shFv@GMT3$#c6yY9N{0{y$;HP&-A
zAZx{?2M6hoL2UqSu*u=4aKQx^C|p_Z#a1$4zyP(cBFsK=#1Tg*pD2w#>+CkP4tpm_
z{T(`^=V*Zc>F;5<*xaWL{Pq)Qs>gMkGI@t~WDkw*+w^_btZmCH_8#^tuZ#nqW52S|
zwc&SsA@DoX^BMGqM(|)UJbcfLY;ZyLL-bs9$SC7Ba|L)KkD}*#0Y7=r>{y`tn
z>qq#3AUj|afp^#!jJ}S4h9%y*o%H&VL|?59$F_cl&!l(#4jB;HGf4L@L1T@p{|c^p
z{SN$D*PbbpKVP-}_OG8(hb*sOgFiqWXa}qQEZYrWT=hX*fInXlHe};^Oy$gjkT-y{14P09_I&87y
zy^i0qHu}m>2jLW`PnyrV%753JbCBk)RI03C6>O!V`hI=g$0E`AMZCl=s`W`Y-olNjtHNdVCiPf_3AbTs7wc&|f+IskImD#!b-?^l=y@V8(+HBDfx4xQYzzq
zya-RG0eokZOi9$kmFehe%LDM_i3xWAWF9x}J07*=(WKwQ#~>+L2t5N?E9pA8@*R!K
z;Q@5gxOHOeOm8{fcYQx2Jb+y&>Ygf@wJ5O8;OzTuOfF|kuEe|S4}^Ei1@@*?0)O{S
zNB44hU}%}lk3SFV&E?|Z-gh*t1P@>nvh^3jkJI_GIlip~-^Te(_94T|rZ7yt5GNk4
zEJx2u@c_P=Ccxg6buv{7U-w-{`#5<3UN+fNInD9D>-#z5fhn>+QGorrZqK6S_)FYT
z;&~Dqk{H`pUU{XsMV|XXjK4=8eN>%G#5tv$Yr=V(05L}SP5emC;2{?1V~;(i&fnn-
z0OG`Q1{LS*64%fsE~sqNILFg0mIY5o*-t#h(JC6R5VMu@NjTe?IH%7(`)spw4-|7J
z^oO$+I76D)tDG%Mj9J%iCEM*LNuy$bjO6xkg^ZPq?ZhJIoFT??eO^gIEN)yZF?*S7
zi23f1K`-0Y^^Zos&2~jPz!*oT=6ph2`kp;|sxuEbLq03Oc|9{`%uw<5}_-NMdWsSzINA?xXfnzeZ%*K>}C|LkTj-{kFo&RC%zlzG-f
zR>w=?vU!1+)HS)UrFZY%HO2S-evGZPTeogmE|R$$v7*$EJ95jw@%&a;}+!<`iH}0o$o5O*5LmPU5ZMarsZ~
z>)oM4hi2!wHgA3ROH+WeB5Pp%;L**@kL;g&AAinX#wP9{8UnMe3|{e1OPTX#j_u>&BfW
z@1LDl{@-JdJ;cI=3sYqy)`;iLo2Sm3M$hQmx3A!w_tvdj8;>!ZE4z<8kG^ZyuF6))
zdBjtvP8Ca*ENOPOb&?fCPs*`Q&|mUCXY(1iO|`$n+|4+fG~mmL@7th3gVg!utf8er
z8_r_x(xr>?y<`4J(ie$yQ;irag{?`yT!-vB@Co-Y~Xuj;z9rUjqI2}_qQ6$1wj6zI`eqZ7Uj?3uymCr)55
zPS$7pl8K)7vpXs$P>@9cRd1txJsotM6
zXO3v!zI`*E$$0w)*5&x)j~6RetSFmiue|b#Xw#-m;5RZtw@mXsG`{Sz%gSy?Y}^-K
zcwtTL4Pj3hd-BVLyY9Lx%lw~d+}Vq}Y15{%nsE8@<>JUAkIXb3Y{y#WPWI223#Xrc
zdQH;LmhN%9M+^|wp2`7YjJUW71vU4TqZ8ma>h>6?yM}$OGTTSqJ=lKKbMmE7Ia4L2Rx#@o{CfoKskb
zD#xz}8&4M3veM#(0RskD@BL>^O$*Oj{SH2mw!j)e4eaYEn+Hda9&JnFnzbdQ(I3&V
z)~s1$)i&6>StCvZZrg*eJw+v4LsRra2lXqz6o#IMJp_RD9W-@8gJ_=i0SvCqDS#
z17mt)k8azxZS44DeM~7AfA;R$>Sn}GF8BMj_&$DaHo%%>Ez&E=I!~WI-KHs2Eb%=0
z@+3)XT{eDmHZj3?N0;HQEp0faG{Gka`yaBEy|EbZEelY7Fs^v>wa?G
zb=R4)zRJ(qzAnRE?c2j<{{TL;j^_3>*R*3_pN-82Uw8a{bvc#1;;Yf4M-MT0@L(~1
z{CF{Y_G}gJ0lp&E1AA<+&0=%mychaAvN!vCtDz3;|1kyjBv+E=dHFUxV*{1kuZ`S{
zk1>4w;)^d@uP0G&{Bn?|*?alzZ+}}*cG_*r7=$co0_1n=jOC60v^C?SDZq1;i0@6T
z5^POz_|}Kj`JH#(5u-+pO5Mh2v&W)y=gz73)wIWkB)`WPY6E@x^huTO>^sIE&(=O`
zQ%CFptTm;zUx)pZrhwd(B!A?+>`9X*nUY-j2Bjq<;6KWEpJeQvQUqY{4zf|wejUV9
zGKK4}zuxh`rR@#*W#!71rX*K!KhhW%i0QU$*)n7Nl6nCh8k~}@dd$Q9w8~6P4&m1A4V7a_lZN7R3G5XOA{bYSW>v;z1+!@C!3I3<-OsC8xp5C
zx@WZmXKp9KADky?0{E#Xfk)oUWN(V;TuZwoIsZfRapT6BG=D|&PM9zucHZM$024rd
z%{#9qQ9sVF2J|bphh6ngTYEq-fl$
zS1&97Bhxy>ou62}=bUp+Y;+kpa-&E5-Qp(H^Q);`OPGhxIuF=xg;
zN=)E5XoK&D32=6J9N(>_GFUUG%}ju}pGn2tH|tKqk;TjPl`xe9@4fe4>w4SLE9>vgojcdtY?1!woUgd+O_}A4y|>?fd#o|hSl44s
zq#PLuA0=BlK@U^Ff5rBDwfdgv*)7B%PJ&iLh72)nZj~S1B~F`*zi!0Zv_0Pl|2%y4
z{AEV@%{dyTKuqB%WxG6$wo#igCaJi4tlgUeu@8Fp?wxp>6U)N357>^$H{N)oDLKS(
z#~qhAE%MZN*dt{M#4C%N2JE@A@nt49SKPRSl}D^X8(GN4M~d}$Y?&s2?3jixRv3M<
zc;9A?H7?t>mtHpe4mguH?L9ZP`=U+dk`qrn(S-F?UT6D>zTT^O&(zk=7?Z>vJDYyU
z{t#V{w66I4+r&a8?z;(&?Bz8D=lgHY|Fnw<;D2w@j#2ZD88gNN4YfzNQt|K6hfM*w
zDXLADJ;yfSWPfx1H;wZ!bLPxM_#fM)2~^V0m-8vHftdn(7)p%?$aAb6n*zE*+OlA&
zx>Wmf&QUXkq~aRhbI(0SsF=zVakm9~UQ8j%*fDnOSkq!ve*A>uu4CTI!`HwRlCrbI
z8zxX`U61qLP4^(P=8PohXA@@~pSC1$iCU(O3>0Lu
z;w((lHF$5sxj6IYdM|B!$t9O~$r$PXq|RQpIa8Q;g;9AmiF`Klqkk=!wQJjTWg2YW
zym=L+K@xOLt4vc}i*`y1H`Z270UJ;fYY`@OWDkJ}Jo3mRiiYR`_(zv>mNM_Un9p`&
zKW%Jkb4+r-FJ}VVK$`v!LH*+X`|meG8##x#Rtpy{G1j?WN#SCYi5SlRHPDU>7c=x;$Qcb2mK*iV}>7n?v7`*ks1DfP^BueZh>
ze!`Yjs{ACL;|rKJ;O`w~M@piOc`cLufhK^wlq4-UH^vs22RQ#B?KMz*4$_7o8&Z<(
zlGn0{S#APJ`Tcw8i!Hkxz;}RsBT4Pe_u|U`>}^RCuvz$VaKG*3AX7iaG-4gv?8Oh_
zKjdRH*`UneOZ;oQ-*fW4Gy_3NbQRZZn`u7e5S`<@2=roIrR>VVCKvm{vWIOB{n
z)EP(YagWk3kf*RsA)8^VKkvNrRP1xkQ!?F~UJB1*&%r-!*)EN?uH-jN&ss)5O#@%X
zR8!oF@lMz?e3FUrK!TUC@9M<8wQJXgy_)KM=G|g`Lus^09=~C_-!P7NHc{z#)>&tz
z%45~|G5TlR@kFE2zZUXUGyg+>)7XF2$iuccA+xauvKIKTQgt+yG18KMG(C`aP1mSx
z`K_AXBL-d6y<(bH?rLrWs#g8i+m`eJ&YY_SK;K}GSQNe7OC$HcIqyR=Q~M`A%9Wrs
z{J`8}6MG~J{f2r?pFUk+ld1%LYxSGexc*hFE&BHDTb7@EHTDJ8``nqmcH?(3UWxLo
z+Wa4);EXMvGY+#1jQ8vvBhEvVI=ei{^bb@2H}-vJHue+@(tw!MMv=H^|@klM!Qc{`~psoFx3FweJsh0iD=ij%^a#FK1j6$CmwO
z?0x6#q@hEH8qYT!J9c#ZUj^g8jgO7(Sq@3^cO}cFZPFmafIXlD$sp3pP8P2;pEpw`5zmQDd2DJ
z_AoZrpEMs<>RK)5ADPylbBIjei7P+r&QCx6v?=>5J8E1wxN_p@M>gkfRWjB*{%^=&
z#2u`ZpS(@nY&uK7e*G%eXI*~@?k_gypH|8S)Td7$@$S3tHjTUZJEYyN?c28(re{Wh
zFfr;}yux4}OuG%e-?KUYdh+DSsq!6ms;N__Hu1gw2mgaKW1w?Jv8~T3a;BH=?*HoP
z3#L9SY3jf|d-g2p!$Vw(q~k(y?!kfu3v|~rzE)CZbbYOOU&F`66qq-Xq$~REgb5QO
z?|H-rI;s7GoJ~ZZw*_}bMRER1l8>U4jT~(XN$PUP9CM5nzcG*DTi?2M>sb0GeViC9
zrg37tv}CUr^@-9BCV7ZQH+uAF(W_T4QAt~v3C`7;*Cys>5__1ijgJ~NN^r(}Ik0;5
zYUQtquac>a2;VgPlhHReZroT78tPsAOp|DLZ>)ImWR9WlbC#Hk6=ycz(B{OHH-UzR
zhS+6pWcXRLW|^Q-GB4}fbLY-gV0ACxbiPqvP2|xPAqNz%rnmzqlzq)1mEPPO!o4bt^?2r*anlL0qe|Jit7M9I1iOI
z;-qK-y=}n76D_R=aK4c#BrR8R)>T&K0ruJ>n|lJ!lqZ0sVY
zfQ-OA~9{b^6?C)OxvZxG{27Ngb#@6>H
zYCUZ8&6qJmV7E$g44`Zqy%+g0sde$B>syKPP4@%9GsF!sU1zeX8|xIT@vL9J-j-Ek
zeGflS;tbi;#q^zRw%JB3U%uQJf7U)szg5fmZDK;uD5su!YHD<5E-;NdV$3*2USLc`
zS3(|1i&yApHo$p*Y2lcM@33}mdM*I%LfpZ$*WK}{gb(9fvn|GBe24iF-25Zk#$RIB7p`Kkfa$k@Yrg
z*kH{5AAkI@(|aW^`{^0=w`pu#-9VMtlUS-wtVglFjbA77Fn&4gd*;e-bQk<4={u#$
zO8fb3x80^mYji~}OdHoceGk7yn{_AlxhA~^lqWhDs|)9!+5j>evHgmb?VgpM8}MDX
z0b(cPPvK}>n#N%?Vm)ESiWN5GS9=%2%60dnw+Z98?cOJJHb?8|wD!^pzft`6ZD7-;
zO@gyPyydz7?b-plu`k31ke#}9>*m_QLI)c*Y?uwn)jLV-%d6%7gHC7@?}o;}9)$g(
zmg#3&cg{1jopbTutwtT*lm^w7pZ$ThK%79gKRep|i*Gajgtkzr^_Obn*SPV)@5W|L
zShpj4kDNYSybo|Dl8x`7rZH<4Y4~8p&0AS158HV;z?s!9_Hl3@z@Ng#zd^SLei_c@
z98B9d`&Up5Lfr;YuAF)9WTEEm#JXF#dmA`|FV1#$Wi@XjO%H6+Ha@uCzQLLEPDg8`
zZ{NOsRa{&zeN6u+?vImug85bRZJbwZnzr@{-L~u#?A*C?&5l|4$r5j)Twgt8CbzdG
zJ>J*-h&?Ff0zO^DZcZvjh{me_
zeuW=t7W^66C&s=}n{y%k^$z;Y8hQ?RA6|c4rs-b($;1EbMUQ(P(0qXWuyEl*r6VL2
zPu!*rSPvO6V1SCRZnIZN*RRwSnI|W_5C7jG(^WR~uJ(7}O=99E4fHd30vQS2GA(_9
zF&;k<_LZaimutU|LZ^unCuUvV_schRl8?`>c3!r{4<0c+Q$7kG6w8C(*h3FJBqmLo
zB=DW*Tnl6}&TBNS2jeWhH1Ik7i}R1L(U#-?R}A;CXXp-T#MLvUPYn6{%QB9Pp&aA3
zP3xQRkWHM{FkUF2e+_y!G$cP*iI%duoh5r{7;?P3(5Ij#m=$KTmQiKbIh1AqS7%%E5viRdKrK7
z|0h=nm#XP=_;L|{6T3%V0Utqh+Zr41{XBL;0RL|-@4a2?eL&Zrec-G&*_=aJZG8uO
z&dixJ#Yrcfly{qspU;DS!{hLJ5R8#GgWjpxd&Ej1_A6^>t5>hCHr5Ws2cE}AbKt;%
zshkIvA-;jf!=SBv@Ucua%kRbNj-Jr9YgaLH5_4C@Q%
z0qZhcSJ!vl_~qpRcp08HflFm(ZyD}?M~|%Xz)%y$-<%g4>GQ4+xG^B(Jis3JFnhmz
zR204?pXPpVfUILy$2Es{@wUU>d2~?@Fa7ZU9Dlg}kx?EPR+_y_=Ga2!xG$sicjII6
z@iEWzfoI{}IAC{K$O@VAOfOfr{IoA|-FA(G@jYd9l>cWwR;a9l4P2SEM^|i(buw)$Q&xce2J9nqdns#=U*j61;2n6#bnl9MrD|YbiOG1G
zTwDp)52E%9^howAc91DAK-`HjGPxLAt{;T;19$@-DHWqcX2~p|AM}uk{_WO~U0=Xw
zXSPhN-@+TU1jOLQW^}1c*mIqXl>h#KOutB`t7SS_rq(h!XrutNz^~$LnXZzFvspQt
z<|Uc%>1M6l>C@HQM`#<`h_<55Xgk`FwxmsITVm~34G^oWp*c3KHpegfy!q`yVMODD
zHU9f;8t>Qp?{C$3-+x=S9`I?)``x+UqWD5VeW0ObeuZgW%llhUPRrZs!!2q>z7c`w
z6bjvi*cepK-hpT;L>q$gOGN8~?n?^Q1>Khw2)a!LLhc7vOI9p^%9PMO?0#T%3x&PI
z?gz9M<+lmD-@wzB(5j*3J?^xs55B*Z>HWIka<+);esJ;buA_jXfTMt;fTMt;fTMt;
zfTMt;KpYgP3oax!dA>fld}F<%A-J4YVfVxIrm*{AdRf^0FugDAewbbvriX^!uL~lC
z)N|{D?n}M6A?Uu;qXP*c4FrJ|mU?<%#iU*zSg{czSizRJgA{dTX9z%4*(CxHRd$R3
igt}9H0Oi!}mK54-S84Hq&32d;H5<`bj-R9o%l`*P
+
+
@@ -709,6 +711,9 @@
+
+
+
diff --git a/SCrawler/SiteResources.Designer.vb b/SCrawler/SiteResources.Designer.vb
index 4f30e04..70442f3 100644
--- a/SCrawler/SiteResources.Designer.vb
+++ b/SCrawler/SiteResources.Designer.vb
@@ -264,6 +264,16 @@ Namespace My.Resources
End Get
End Property
+ '''
+ ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
+ '''
+ Friend Shared ReadOnly Property ThreadsIcon_192() As System.Drawing.Icon
+ Get
+ Dim obj As Object = ResourceManager.GetObject("ThreadsIcon_192", resourceCulture)
+ Return CType(obj,System.Drawing.Icon)
+ End Get
+ End Property
+
'''
''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
'''
diff --git a/SCrawler/SiteResources.resx b/SCrawler/SiteResources.resx
index 73de61b..b86f2da 100644
--- a/SCrawler/SiteResources.resx
+++ b/SCrawler/SiteResources.resx
@@ -178,6 +178,9 @@
Content\Pictures\SitePictures\ThisVidPic_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
+ Content\Icons\SiteIcons\ThreadsIcon_192.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
+
Content\Icons\SiteIcons\TikTokIcon_32.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a