mirror of
https://github.com/AAndyProgram/SCrawler.git
synced 2026-03-14 15:52:18 +00:00
YT Update 'ReplaceModificationDate' SCrawler API.Instagram: fix 'LastCursor' issue API.Reddit: add OAuth validation; add default credentials; hide unused controls; add 'SeparatedTasks'; bypass 429 error; fix crossposts downloading API.Redgifs: force delete cookies if user added them API.TikTok: yt-dlp modification (date change) API.Twitter: simplify large profiles download SettingsCLS: change default max value for channel downloads
384 lines
22 KiB
VB.net
384 lines
22 KiB
VB.net
' 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 System.Reflection
|
|
Imports PersonalUtilities.Tools.Web.Clients
|
|
Imports PersonalUtilities.Tools.Web.Clients.Base
|
|
Imports PersonalUtilities.Tools.Web.Documents.JSON
|
|
Imports PersonalUtilities.Functions.XML
|
|
Imports PersonalUtilities.Functions.RegularExpressions
|
|
Imports DN = SCrawler.API.Base.DeclaredNames
|
|
Imports DownDetector = SCrawler.API.Base.DownDetector
|
|
Imports Download = SCrawler.Plugin.ISiteSettings.Download
|
|
Namespace API.Reddit
|
|
<Manifest(RedditSiteKey), SavedPosts, SeparatedTasks, SpecialForm(False), UseDownDetector>
|
|
Friend Class SiteSettings : Inherits SiteSettingsBase : Implements DownDetector.IDownDetector
|
|
#Region "Declarations"
|
|
#Region "Authorization"
|
|
Private Const ApiClientID_Default As String = "dYctRA-SIJxyykHe27lGZg"
|
|
Private Const ApiClientSecret_Default As String = "_5D6KzplRPDga-es1YlpzDIe9hiFlg"
|
|
<PropertyOption(ControlText:="Login", ControlToolTip:="Your authorization username", IsAuth:=True), PXML, PClonable(Clone:=False)>
|
|
Friend ReadOnly Property AuthUserName As PropertyValue
|
|
<PropertyOption(ControlText:="Password", ControlToolTip:="Your authorization password", IsAuth:=True), PXML, PClonable(Clone:=False)>
|
|
Friend ReadOnly Property AuthPassword As PropertyValue
|
|
<PropertyOption(ControlText:="Client ID", ControlToolTip:="Your registered app client ID", IsAuth:=True), PXML, PClonable(Clone:=False)>
|
|
Friend ReadOnly Property ApiClientID As PropertyValue
|
|
<PropertyUpdater(NameOf(ApiClientID))> Private Function ApiClientID_SetDefault() As Boolean
|
|
ApiClientID.Value = ApiClientID_Default
|
|
Return True
|
|
End Function
|
|
<PropertyOption(ControlText:="Client Secret", ControlToolTip:="Your registered app client secret", IsAuth:=True), PXML, PClonable(Clone:=False)>
|
|
Friend ReadOnly Property ApiClientSecret As PropertyValue
|
|
<PropertyUpdater(NameOf(ApiClientSecret))> Private Function ApiClientSecret_SetDefault() As Boolean
|
|
ApiClientSecret.Value = ApiClientSecret_Default
|
|
Return True
|
|
End Function
|
|
<PropertyOption(ControlText:="Bearer token",
|
|
ControlToolTip:="Bearer token (can be null)." & vbCr &
|
|
"If you are using cookies to download the timeline, it is highly recommended that you add a token." & vbCr &
|
|
"You can find different tokens in the responses. Make sure that bearer token belongs to Reddit and not RedGifs." & vbCr &
|
|
"There is not need to add a token if you are not using cookies to download the timeline.", IsAuth:=True)>
|
|
Friend ReadOnly Property BearerToken As PropertyValue
|
|
<PropertyOption(ControlText:="Use 'cUrl' to get a token", IsAuth:=True), PXML, PClonable, HiddenControl>
|
|
Private ReadOnly Property BearerTokenUseCurl As PropertyValue
|
|
#Region "TokenUpdateInterval"
|
|
<PropertyOption(ControlText:="Token refresh interval", ControlToolTip:="Interval (in minutes) to refresh the token",
|
|
AllowNull:=False, LeftOffset:=120, IsAuth:=True), PXML, PClonable>
|
|
Friend ReadOnly Property TokenUpdateInterval As PropertyValue
|
|
<Provider(NameOf(TokenUpdateInterval), FieldsChecker:=True)>
|
|
Private ReadOnly Property TokenUpdateIntervalProvider As IFormatProvider
|
|
#End Region
|
|
<PXML, PClonable> Private ReadOnly Property BearerTokenDateUpdate As PropertyValue
|
|
<PropertyOption(ControlText:="Use the token to download the timeline", IsAuth:=True), PXML, PClonable>
|
|
Friend ReadOnly Property UseTokenForTimelines As PropertyValue
|
|
<PropertyOption(ControlText:="Use the token to download saved posts", IsAuth:=True), PXML, PClonable>
|
|
Friend ReadOnly Property UseTokenForSavedPosts As PropertyValue
|
|
<PropertyOption(ControlText:="Use cookies to download the timeline", IsAuth:=True), PXML, PClonable>
|
|
Friend ReadOnly Property UseCookiesForTimelines As PropertyValue
|
|
<PropertyOption(ControlText:=DeclaredNames.SavedPostsUserNameCaption, ControlToolTip:=DeclaredNames.SavedPostsUserNameToolTip, IsAuth:=True), PXML, PClonable(Clone:=False)>
|
|
Friend ReadOnly Property SavedPostsUserName As PropertyValue
|
|
Friend ReadOnly Property CredentialsExists As Boolean
|
|
Get
|
|
Return {AuthUserName.Value, AuthPassword.Value, ApiClientID.Value, ApiClientSecret.Value}.All(Function(v$) Not v.IsEmptyString)
|
|
End Get
|
|
End Property
|
|
<PropertiesDataChecker({NameOf(AuthUserName), NameOf(AuthPassword), NameOf(ApiClientID), NameOf(ApiClientSecret),
|
|
NameOf(UseTokenForTimelines), NameOf(UseCookiesForTimelines)})>
|
|
Private Function OAuthCredentialsChecker(ByVal p As IEnumerable(Of PropertyData)) As Boolean
|
|
Const msgTitle$ = "OAuth credentials"
|
|
If p.ListExists Then
|
|
Dim useToken As Boolean = False, useCookies As Boolean = False
|
|
Dim d$ = String.Empty
|
|
Dim dCount As Byte = 0
|
|
Dim members As IEnumerable(Of MemberInfo) = GetObjectMembers(Me)
|
|
Dim getPropText As Func(Of String, String) = Function(name) members.First(Function(m) m.Name = name).GetCustomAttribute(Of PropertyOption).ControlText
|
|
Dim dataStr As Action(Of String, String) = Sub(dd, name) If dd.IsEmptyString Then d.StringAppendLine(getPropText(name)) : dCount += 1
|
|
For Each pp As PropertyData In p
|
|
Select Case pp.Name
|
|
Case NameOf(AuthUserName) : dataStr(pp.Value, NameOf(AuthUserName))
|
|
Case NameOf(AuthPassword) : dataStr(pp.Value, NameOf(AuthPassword))
|
|
Case NameOf(ApiClientID) : dataStr(pp.Value, NameOf(ApiClientID))
|
|
Case NameOf(ApiClientSecret) : dataStr(pp.Value, NameOf(ApiClientSecret))
|
|
Case NameOf(UseTokenForTimelines) : useToken = pp.Value
|
|
Case NameOf(UseCookiesForTimelines) : useCookies = pp.Value
|
|
Case Else : Throw New ArgumentException($"Property name '{pp.Name}' is not implemented", "Property Name")
|
|
End Select
|
|
Next
|
|
If d.IsEmptyString Then
|
|
If useToken And useCookies Then
|
|
Return True
|
|
Else
|
|
If Not useToken Then d.StringAppendLine(getPropText(NameOf(UseTokenForTimelines)))
|
|
If Not useCookies Then d.StringAppendLine(getPropText(NameOf(UseCookiesForTimelines)))
|
|
MsgBoxE({$"You need to check the following options:{vbCr}{d}", msgTitle}, vbCritical)
|
|
Return False
|
|
End If
|
|
ElseIf dCount = 4 Then
|
|
Return MsgBoxE({$"You haven't configured OAuth. It's highly recommended to use OAuth.{vbCr}Do you still want to continue?", msgTitle},
|
|
vbExclamation,,, {"Process", "Cancel"}) = 0
|
|
Else
|
|
MsgBoxE({$"You haven't filled in the following fields:{vbCr}{d}.{vbCr}{vbCr}" &
|
|
"To use OAuth authorization, you must fill in all authorization fields.", msgTitle}, vbCritical)
|
|
Return False
|
|
End If
|
|
End If
|
|
Return True
|
|
End Function
|
|
#End Region
|
|
#Region "Other"
|
|
<PropertyOption(ControlText:="Use M3U8", ControlToolTip:="Use M3U8 or mp4 for Reddit videos"), PXML, PClonable, HiddenControl>
|
|
Friend ReadOnly Property UseM3U8 As PropertyValue
|
|
<PropertyOption(ControlText:="Check image", ControlToolTip:="Check the image if it exists before downloading (it makes downloading very slow)"), PXML, PClonable, HiddenControl>
|
|
Friend ReadOnly Property CheckImage As PropertyValue
|
|
<PropertyOption(ControlText:="Check image: get original", ControlToolTip:="Get the original image if it exists"), PXML, PClonable, HiddenControl>
|
|
Friend ReadOnly Property CheckImageReturnOrig As PropertyValue
|
|
<PropertyOption(ControlText:=DN.ConcurrentDownloadsCaption,
|
|
ControlToolTip:=DN.ConcurrentDownloadsToolTip, AllowNull:=False), PXML, TaskCounter, PClonable>
|
|
Friend ReadOnly Property ConcurrentDownloads As PropertyValue
|
|
#End Region
|
|
#Region "IDownDetector Support"
|
|
Private ReadOnly Property IDownDetector_Value As Integer Implements DownDetector.IDownDetector.Value
|
|
Get
|
|
Return 100
|
|
End Get
|
|
End Property
|
|
Private ReadOnly Property IDownDetector_AddToLog As Boolean Implements DownDetector.IDownDetector.AddToLog
|
|
Get
|
|
Return False
|
|
End Get
|
|
End Property
|
|
Private ReadOnly Property IDownDetector_CheckSite As String Implements DownDetector.IDownDetector.CheckSite
|
|
Get
|
|
Return "reddit"
|
|
End Get
|
|
End Property
|
|
Private Function IDownDetector_Available(ByVal What As Download, ByVal Silent As Boolean) As Boolean Implements DownDetector.IDownDetector.Available
|
|
Return MDD.Available(What, Silent)
|
|
End Function
|
|
#End Region
|
|
#End Region
|
|
#Region "Initializer"
|
|
Friend Sub New(ByVal AccName As String, ByVal Temp As Boolean)
|
|
MyBase.New(RedditSite, "reddit.com", AccName, Temp, My.Resources.SiteResources.RedditIcon_128, My.Resources.SiteResources.RedditPic_512)
|
|
|
|
Dim token$
|
|
With Responser
|
|
Dim d% = .Decoders.Count
|
|
.Decoders.ListAddList({SymbolsConverter.Converters.Unicode, SymbolsConverter.Converters.HTML}, LAP.NotContainsOnly)
|
|
.Accept = "application/json"
|
|
token = .Headers.Value(DeclaredNames.Header_Authorization)
|
|
End With
|
|
|
|
AuthUserName = New PropertyValue(String.Empty, GetType(String))
|
|
AuthPassword = New PropertyValue(String.Empty, GetType(String))
|
|
ApiClientID = New PropertyValue(String.Empty, GetType(String))
|
|
ApiClientSecret = New PropertyValue(String.Empty, GetType(String))
|
|
BearerToken = New PropertyValue(token, GetType(String), Sub(v) Responser.Headers.Add(DeclaredNames.Header_Authorization, v))
|
|
BearerTokenUseCurl = New PropertyValue(False)
|
|
TokenUpdateInterval = New PropertyValue(360)
|
|
TokenUpdateIntervalProvider = New TokenRefreshIntervalProvider
|
|
BearerTokenDateUpdate = New PropertyValue(Now.AddYears(-1))
|
|
UseTokenForTimelines = New PropertyValue(False)
|
|
UseTokenForSavedPosts = New PropertyValue(False)
|
|
UseCookiesForTimelines = New PropertyValue(False)
|
|
SavedPostsUserName = New PropertyValue(String.Empty, GetType(String))
|
|
|
|
UseM3U8 = New PropertyValue(True)
|
|
CheckImage = New PropertyValue(False)
|
|
CheckImageReturnOrig = New PropertyValue(True)
|
|
ConcurrentDownloads = New PropertyValue(1)
|
|
|
|
MDD = New MyDownDetector(Me)
|
|
|
|
UrlPatternUser = "https://www.reddit.com/{0}/{1}/"
|
|
ImageVideoContains = "reddit.com"
|
|
UserRegex = RParams.DM("[htps:/]{7,8}.*?reddit.com/([user]{1,4})/([^/\?&]+)", 0, RegexReturn.ListByMatch, EDP.ReturnValue)
|
|
End Sub
|
|
Private Const SettingsVersionCurrent As Integer = 3
|
|
Friend Overrides Sub EndInit()
|
|
If CInt(SettingsVersion.Value) < SettingsVersionCurrent Then
|
|
SettingsVersion.Value = SettingsVersionCurrent
|
|
UseM3U8.Value = True
|
|
CheckImage.Value = False
|
|
CheckImageReturnOrig.Value = True
|
|
BearerTokenUseCurl.Value = False
|
|
End If
|
|
MyBase.EndInit()
|
|
End Sub
|
|
#End Region
|
|
#Region "GetInstance"
|
|
Friend Overrides Function GetInstance(ByVal What As Download) As IPluginContentProvider
|
|
Return New UserData
|
|
End Function
|
|
#End Region
|
|
#Region "DownloadStarted, ReadyToDownload, Available, DownloadDone, UpdateRedGifsToken"
|
|
Private ReadOnly MDD As MyDownDetector
|
|
Private Class MyDownDetector : Inherits DownDetector.Checker(Of SiteSettings)
|
|
Private __TrueValue As Boolean = False
|
|
Friend Sub New(ByRef _Source As SiteSettings)
|
|
MyBase.New(_Source)
|
|
End Sub
|
|
Protected Overrides Function AvailableImpl(ByVal What As Download, ByVal Silent As Boolean) As Boolean
|
|
__TrueValue = Source.AvailableTrueValue(What)
|
|
Return MyBase.AvailableImpl(What, Silent)
|
|
End Function
|
|
Protected Overrides Function AvailableImpl_TRUE() As Boolean
|
|
Return AvailableImpl_TrueValueReturn()
|
|
End Function
|
|
Protected Overrides Function AvailableImpl_FALSE_SILENT_NOT_MSG_YES() As Boolean
|
|
Return AvailableImpl_TrueValueReturn()
|
|
End Function
|
|
Private Function AvailableImpl_TrueValueReturn() As Boolean
|
|
If __TrueValue Then Source.UpdateRedGifsToken()
|
|
Return __TrueValue AndAlso Source.UpdateTokenIfRequired()
|
|
End Function
|
|
Friend Overrides Sub Reset()
|
|
__TrueValue = False
|
|
MyBase.Reset()
|
|
End Sub
|
|
End Class
|
|
Friend Property SessionInterrupted As Boolean = False
|
|
Friend Property RequestCount As Integer = 0
|
|
Friend Overrides Function ReadyToDownload(ByVal What As Download) As Boolean
|
|
If What = Download.Main Then
|
|
Return Not SessionInterrupted
|
|
Else
|
|
Return True
|
|
End If
|
|
End Function
|
|
Friend Overrides Function Available(ByVal What As Download, ByVal Silent As Boolean) As Boolean
|
|
Return AvailableTrueValue(What) AndAlso UpdateTokenIfRequired()
|
|
End Function
|
|
Private Function AvailableTrueValue(ByVal What As Download) As Boolean
|
|
Return Not What = Download.SavedPosts OrElse (Responser.CookiesExists And ACheck(SavedPostsUserName.Value))
|
|
End Function
|
|
Friend Overrides Sub DownloadDone(ByVal What As Download)
|
|
SessionInterrupted = False
|
|
RequestCount = 0
|
|
MDD.Reset()
|
|
MyBase.DownloadDone(What)
|
|
End Sub
|
|
Private Sub UpdateRedGifsToken()
|
|
Settings(RedGifs.RedGifsSiteKey).ListForEach(Sub(h, i) DirectCast(h.Source, RedGifs.SiteSettings).UpdateTokenIfRequired())
|
|
End Sub
|
|
#End Region
|
|
#Region "IsMyUser, GetUserUrl, GetUserPostUrl"
|
|
Friend Const ChannelOption As String = "r"
|
|
Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions
|
|
Dim l As List(Of String) = RegexReplace(UserURL, UserRegex)
|
|
If l.ListExists(3) Then
|
|
Dim n$ = l(2)
|
|
If Not l(1).IsEmptyString AndAlso l(1) = ChannelOption Then n &= $"@{ChannelOption}"
|
|
Return New ExchangeOptions(Site, n)
|
|
Else
|
|
Return Nothing
|
|
End If
|
|
End Function
|
|
Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String
|
|
With DirectCast(User, UserData) : Return String.Format(UrlPatternUser, IIf(.IsChannel, ChannelOption, "user"), .NameTrue) : End With
|
|
End Function
|
|
Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String
|
|
If Not Media.Post.ID.IsEmptyString Then
|
|
Return $"https://www.reddit.com/comments/{Media.Post.ID.Split("_").LastOrDefault}/"
|
|
Else
|
|
Return String.Empty
|
|
End If
|
|
End Function
|
|
#End Region
|
|
#Region "UserOptions"
|
|
Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean)
|
|
If Options Is Nothing OrElse Not TypeOf Options Is RedditViewExchange Then Options = New RedditViewExchange(Me)
|
|
If OpenForm Then
|
|
Using f As New RedditViewSettingsForm(Options, True) : f.ShowDialog() : End Using
|
|
End If
|
|
End Sub
|
|
#End Region
|
|
#Region "BeginEdit, Update"
|
|
Private _OldTokenValue As String = String.Empty
|
|
Friend Overrides Sub BeginEdit()
|
|
_OldTokenValue = BearerToken.Value
|
|
MyBase.BeginEdit()
|
|
End Sub
|
|
Friend Overrides Sub Update()
|
|
If _SiteEditorFormOpened Then
|
|
Dim newTokenValue$ = BearerToken.Value
|
|
If Not newTokenValue.IsEmptyString AndAlso Not newTokenValue = _OldTokenValue Then BearerTokenDateUpdate.Value = Now
|
|
End If
|
|
MyBase.Update()
|
|
End Sub
|
|
#End Region
|
|
#Region "Token"
|
|
Private Function UpdateTokenIfRequired() As Boolean
|
|
UpdateRedGifsToken()
|
|
If (CBool(UseTokenForTimelines.Value) Or CBool(UseTokenForSavedPosts.Value)) AndAlso CredentialsExists Then
|
|
If CDate(BearerTokenDateUpdate.Value).AddMinutes(TokenUpdateInterval.Value) <= Now Then Return UpdateToken()
|
|
End If
|
|
Return True
|
|
End Function
|
|
Private Overloads Function UpdateToken() As Boolean
|
|
Return UpdateToken(AuthUserName.Value, AuthPassword.Value, ApiClientID.Value, ApiClientSecret.Value, EDP.SendToLog + EDP.ReturnValue)
|
|
End Function
|
|
<PropertyUpdater(NameOf(BearerToken), {NameOf(AuthUserName), NameOf(AuthPassword), NameOf(ApiClientID), NameOf(ApiClientSecret)})>
|
|
Private Overloads Function UpdateToken(ByVal UserName As String, ByVal Password As String, ByVal ClientID As String, ByVal ClientSecret As String) As Boolean
|
|
Return UpdateToken(UserName, Password, ClientID, ClientSecret, EDP.LogMessageValue)
|
|
End Function
|
|
Private Overloads Function UpdateToken(ByVal UserName As String, ByVal Password As String, ByVal ClientID As String, ByVal ClientSecret As String, ByVal e As ErrorsDescriber) As Boolean
|
|
Try
|
|
Dim result As Boolean = True
|
|
If {UserName, Password, ClientID, ClientSecret}.All(Function(v) Not v.IsEmptyString) Then
|
|
result = False
|
|
Dim r$ = String.Empty
|
|
Dim c% = 0
|
|
Dim useCurl As Boolean = Settings.CurlFile.Exists And CBool(BearerTokenUseCurl.Value)
|
|
Dim curlUsed As Boolean = useCurl
|
|
Do
|
|
c += 1
|
|
Using resp As New Responser With {
|
|
.Method = "POST",
|
|
.ProcessExceptionDecision = Function(ByVal status As IResponserStatus, ByVal nullArg As Object, ByVal currErr As ErrorsDescriber) As ErrorsDescriber
|
|
If status.StatusCode = 429 Then
|
|
useCurl = False
|
|
Return EDP.ReturnValue
|
|
ElseIf status.StatusCode = Net.HttpStatusCode.Forbidden And Not useCurl And Settings.CurlFile.Exists Then
|
|
useCurl = True
|
|
Return EDP.ReturnValue
|
|
Else
|
|
Return currErr
|
|
End If
|
|
End Function
|
|
}
|
|
With resp
|
|
If useCurl Then
|
|
If Settings.CurlFile.Exists Then
|
|
curlUsed = True
|
|
.Mode = Responser.Modes.Curl
|
|
.CurlPath = Settings.CurlFile
|
|
.CurlArgumentsLeft = $"-d ""grant_type=password&username={UserName}&password={Password}"" --user ""{ClientID}:{ClientSecret}"""
|
|
Else
|
|
Throw New ArgumentNullException("cUrl file", "The path to the cUrl file is not specified")
|
|
End If
|
|
Else
|
|
.Mode = Responser.Modes.Default
|
|
With .PayLoadValues
|
|
.Add("grant_type", "password")
|
|
.Add("username", UserName)
|
|
.Add("password", Password)
|
|
End With
|
|
.CredentialsUserName = ClientID
|
|
.CredentialsPassword = ClientSecret
|
|
.PreAuthenticate = True
|
|
End If
|
|
End With
|
|
r = resp.GetResponse("https://www.reddit.com/api/v1/access_token",, EDP.ThrowException)
|
|
End Using
|
|
If Not r.IsEmptyString Then
|
|
Using j As EContainer = JsonDocument.Parse(r)
|
|
If j.ListExists Then
|
|
Dim newToken$ = j.Value("access_token")
|
|
If Not newToken.IsEmptyString Then
|
|
BearerToken.Value = $"Bearer {newToken}"
|
|
BearerTokenDateUpdate.Value = Now
|
|
Responser.SaveSettings()
|
|
result = True
|
|
End If
|
|
End If
|
|
End Using
|
|
End If
|
|
Loop While c < 5 And Not result
|
|
End If
|
|
Return result
|
|
Catch ex As Exception
|
|
Return ErrorsDescriber.Execute(e, ex, "[Reddit.SiteSettings.UpdateToken]", False)
|
|
End Try
|
|
End Function
|
|
#End Region
|
|
End Class
|
|
End Namespace |