From bdc73213316fc2a7ceb19379127580f3b6dab534 Mon Sep 17 00:00:00 2001 From: Andy <88590076+AAndyProgram@users.noreply.github.com> Date: Wed, 16 Nov 2022 13:41:45 +0300 Subject: [PATCH] 2022.11.16.0 Add sites: PornHub, XHamster Add saved xvideos posts downloading PluginProvider: added TaskGroup attribute; added IUserMedia inteface; changed PluginUserMedia to IUserMedia in interface declarations; changed 'User' String to IPluginContentProvider in ISiteSettings sinterface Added update the 'LOG' button at the end of the ProfileSaved download function API.Base: added 'IUserMedia' compatibility for 'UserMedia'; moved 'GetImage' from 'UserPost' to 'ChannelsViewForm'; update constants in UserDataBase; updated UserDataBase to new UserInfo environment. API.Instagram.UserData: fixed date issue API.Reddit.SiteSettings: update user patterns API.Twitter.Declarations: moved provider here from MainFrame UserDataBind: updated to new UserInfo environment ActiveDownloadingProgress: updated form rendering AutoDownloader: added SpecialDelay TDownloader: added 'Suspended' option; updated for TaskGroups CollectionEditorForm: fixed order bug LabelsForm: remove old stuff UserEditorForm: added collection editing MainFrame: improve label selection Add import users Added the ability to create a virtual collection and add a virtual user to a real collection SettingsCLS: improve users loading --- .gitignore | 1 + Changelog.md | 34 + HowToSupport.md | 2 +- ProgramScreenshots/SavedPosts.png | Bin 7869 -> 13932 bytes ProgramScreenshots/SettingsSitePornHub.png | Bin 0 -> 16287 bytes ProgramScreenshots/SettingsSiteXHamster.png | Bin 0 -> 14196 bytes ProgramScreenshots/SettingsSiteXvideos.png | Bin 14780 -> 15488 bytes ProgramsComparison.md | 2 +- README.md | 19 +- .../Attributes/Attributes.vb | 17 +- .../Interfaces/IPluginContentProvider.vb | 4 +- .../Interfaces/ISiteSettings.vb | 4 +- .../My Project/AssemblyInfo.vb | 4 +- .../Objects/PluginUserMedia.vb | 59 +- SCrawler/API/Base/M3U8Base.vb | 64 ++ SCrawler/API/Base/ProfileSaved.vb | 3 +- SCrawler/API/Base/SiteSettingsBase.vb | 17 +- SCrawler/API/Base/Structures.vb | 131 +++- SCrawler/API/Base/UserDataBase.vb | 170 +++-- SCrawler/API/BaseObjects/DomainEnvir.vb | 86 +++ SCrawler/API/Imgur/Envir.vb | 2 +- SCrawler/API/Instagram/SiteSettings.vb | 22 +- SCrawler/API/Instagram/UserData.vb | 7 +- SCrawler/API/LPSG/UserData.vb | 5 +- SCrawler/API/PornHub/Declarations.vb | 44 ++ SCrawler/API/PornHub/M3U8.vb | 42 ++ SCrawler/API/PornHub/OptionsForm.Designer.vb | 118 ++++ .../OptionsForm.resx} | 3 + SCrawler/API/PornHub/OptionsForm.vb | 34 + SCrawler/API/PornHub/SiteSettings.vb | 123 ++++ SCrawler/API/PornHub/UserData.vb | 656 ++++++++++++++++++ SCrawler/API/PornHub/UserExchangeOptions.vb | 23 + SCrawler/API/Reddit/M3U8.vb | 2 +- SCrawler/API/Reddit/SiteSettings.vb | 27 +- SCrawler/API/Reddit/UserData.vb | 6 +- SCrawler/API/Redgifs/Declarations.vb | 2 +- SCrawler/API/Redgifs/SiteSettings.vb | 23 +- SCrawler/API/Redgifs/UserData.vb | 19 +- SCrawler/API/TikTok/UserData.vb | 2 +- SCrawler/API/Twitter/Declarations.vb | 9 +- SCrawler/API/Twitter/SiteSettings.vb | 39 +- SCrawler/API/Twitter/UserData.vb | 4 +- SCrawler/API/UserDataBind.vb | 148 ++-- SCrawler/API/XVIDEOS/Declarations.vb | 13 +- SCrawler/API/XVIDEOS/M3U8.vb | 39 +- SCrawler/API/XVIDEOS/SettingsForm.Designer.vb | 80 --- SCrawler/API/XVIDEOS/SettingsForm.vb | 70 -- SCrawler/API/XVIDEOS/SiteSettings.vb | 102 ++- SCrawler/API/XVIDEOS/UserData.vb | 164 +++-- SCrawler/API/Xhamster/Declarations.vb | 19 + SCrawler/API/Xhamster/M3U8.vb | 79 +++ SCrawler/API/Xhamster/SiteSettings.vb | 155 +++++ SCrawler/API/Xhamster/UserData.vb | 344 +++++++++ SCrawler/Channels/ChannelViewForm.Designer.vb | 6 + SCrawler/Channels/ChannelViewForm.vb | 13 +- .../Icons/SiteIcons/PornHubIcon_16.ico | Bin 0 -> 1406 bytes .../Icons/SiteIcons/XhamsterIcon_32.ico | Bin 0 -> 15086 bytes .../Pictures/SitePictures/PornHubPic_16.png | Bin 0 -> 764 bytes .../Pictures/SitePictures/XhamsterPic_32.png | Bin 0 -> 1566 bytes .../ActiveDownloadingProgress.Designer.vb | 8 +- .../Download/ActiveDownloadingProgress.vb | 16 +- .../Download/Automation/AutoDownloader.vb | 28 +- SCrawler/Download/Feed/DownloadFeedForm.vb | 2 +- SCrawler/Download/Feed/FeedVideo.vb | 2 +- SCrawler/Download/Groups/DownloadGroup.vb | 1 - SCrawler/Download/TDownloader.vb | 20 +- SCrawler/Download/WebClient2.vb | 50 ++ SCrawler/Editors/CollectionEditorForm.vb | 9 +- SCrawler/Editors/GlobalSettingsForm.vb | 2 +- SCrawler/Editors/LabelsForm.vb | 28 +- SCrawler/Editors/SiteEditorForm.vb | 6 +- SCrawler/Editors/UserCreatorForm.Designer.vb | 374 +++++----- SCrawler/Editors/UserCreatorForm.resx | 25 +- SCrawler/Editors/UserCreatorForm.vb | 312 ++++++--- SCrawler/EncryptCookies.vb | 2 +- SCrawler/MainFrame.vb | 335 ++++++--- SCrawler/MainFrameObjects.vb | 19 +- SCrawler/MainMod.vb | 6 +- SCrawler/My Project/AssemblyInfo.vb | 4 +- .../PluginsEnvironment/Hosts/PluginHost.vb | 2 + .../Hosts/PropertyValueHost.vb | 2 +- .../PluginsEnvironment/Hosts/SettingsHost.vb | 15 +- .../PluginsEnvironment/Hosts/UserDataHost.vb | 14 +- SCrawler/SCrawler.vbproj | 47 +- SCrawler/SettingsCLS.vb | 16 +- SCrawler/SiteResources.Designer.vb | 40 ++ SCrawler/SiteResources.resx | 12 + SCrawler/UserFinder.vb | 334 +++++++++ SCrawler/UserInfo.vb | 66 +- SCrawler/UserSearchForm.vb | 1 + 90 files changed, 3831 insertions(+), 1028 deletions(-) create mode 100644 ProgramScreenshots/SettingsSitePornHub.png create mode 100644 ProgramScreenshots/SettingsSiteXHamster.png create mode 100644 SCrawler/API/Base/M3U8Base.vb create mode 100644 SCrawler/API/BaseObjects/DomainEnvir.vb create mode 100644 SCrawler/API/PornHub/Declarations.vb create mode 100644 SCrawler/API/PornHub/M3U8.vb create mode 100644 SCrawler/API/PornHub/OptionsForm.Designer.vb rename SCrawler/API/{XVIDEOS/SettingsForm.resx => PornHub/OptionsForm.resx} (97%) create mode 100644 SCrawler/API/PornHub/OptionsForm.vb create mode 100644 SCrawler/API/PornHub/SiteSettings.vb create mode 100644 SCrawler/API/PornHub/UserData.vb create mode 100644 SCrawler/API/PornHub/UserExchangeOptions.vb delete mode 100644 SCrawler/API/XVIDEOS/SettingsForm.Designer.vb delete mode 100644 SCrawler/API/XVIDEOS/SettingsForm.vb create mode 100644 SCrawler/API/Xhamster/Declarations.vb create mode 100644 SCrawler/API/Xhamster/M3U8.vb create mode 100644 SCrawler/API/Xhamster/SiteSettings.vb create mode 100644 SCrawler/API/Xhamster/UserData.vb create mode 100644 SCrawler/Content/Icons/SiteIcons/PornHubIcon_16.ico create mode 100644 SCrawler/Content/Icons/SiteIcons/XhamsterIcon_32.ico create mode 100644 SCrawler/Content/Pictures/SitePictures/PornHubPic_16.png create mode 100644 SCrawler/Content/Pictures/SitePictures/XhamsterPic_32.png create mode 100644 SCrawler/Download/WebClient2.vb create mode 100644 SCrawler/UserFinder.vb diff --git a/.gitignore b/.gitignore index 869ef14..480c100 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ bld/ [Ll]og/ [Ll]ogs/ ffmpeg/ +cURL/ Info/ Hidden/ diff --git a/Changelog.md b/Changelog.md index 406f621..42f8279 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,37 @@ +# 2022.11.16.0 + +*2022-11-16* + +**ATTENTION! This version makes changes to the base SCrawler user configuration file. Since you started using this version, you still can downgrade. BUT! Once you add a virtual collection or a virtual user to a collection, you won't be able to downgrade without losing data.** + +- Added + - **PornHub** + - **XHamster** + - An ability to download saved XVIDEOS posts + - Download indicator. While downloading, the rainbow tray icon changed to a blue arrow. + - Collections: the ability to edit a collection using a form + - Collections: the ability to create a **`virtual collection`** and add a **`virtual user`** to a real collection + - Collections: an easier way to added users to a collection + - Collections: an easier way to create collections + - Added icons for channels form context menu buttons + - More convenient change of user labels from the context menu of the user list + - Notifications: complete transition from default notifications to ToastNotifications + - Notifications: when you click on the notification that some of the channels are downloaded, the channels form opens + - Notifications: when you click on the notification that all users are downloaded, the main window form opens + - Notifications: when you click on the notification that the saved posts are downloaded, the saved posts form opens + - Import users + - Minor improvements +- Plugins + - Added + - `TaskGroup` attribute + - `IUserMedia` interface + - Changed + - `GetUserUrl` and `GetUserPostUrl` functions: `String UserName` and `String UserID` changed to ` IPluginContentProvider User` +- Fixed + - Collections editor: new added collections are still not added to the top of the collections list + - Users search form doesn't remember last size + - Minor bugs + # 2022.10.23.0 *2022-10-23* diff --git a/HowToSupport.md b/HowToSupport.md index 04df544..0a4eec5 100644 --- a/HowToSupport.md +++ b/HowToSupport.md @@ -1,7 +1,7 @@ Your support is very valuable to me. Any support is greatly appreciated. Your support encourages me to make new features, update the program, add new sites, etc. You can support the program by: - - **Bitcoin**: bitcoin:BC1Q0NH839FT5TA44DD7L7RLR97XDQAG9V8D6N7XET + - **Bitcoin**: BC1Q0NH839FT5TA44DD7L7RLR97XDQAG9V8D6N7XET - :heavy_dollar_sign: make a donation on this site: https://ko-fi.com/andyprogram - :repeat: make a post about my program on your profile (Reddit, Twitter, Instagram and any other social networks) - :speech_balloon: tell your friends about the program diff --git a/ProgramScreenshots/SavedPosts.png b/ProgramScreenshots/SavedPosts.png index 09de8243be8240afd56998a9d6b4e8d603ea6d8b..f958f627d653de3198567c3ab5f2e10dc64983fa 100644 GIT binary patch literal 13932 zcmeHu2T)U6+ipB47QlvrfD{D*>Ae?~CS3(75(pTINRuulV4;Z8TS8Z9(uF8Jp-LwR z2mv99(tGG3gmO1{&iTLpod16J&b{-`+?jj!Oa`*{-fOMB_TKOFKF{+e?2euW-6`f% zAP|U7^S0_e5a`Gh@ToXK4YXL@+)M-h9C5#=aSK$8xjYMeIcj%P_a+Eb8bz~Xbqx4^ z^2u#ecM#}w)4}IRtIG##5J>8Xrs_?@rxuH&4vvPCdowHQP>H+9aT>y-+EjS+QC;5; zz0&FyGX-%cC_&Ghtz!+KTzq#fUd-pxk_%~5i(ni|UM8Hqy$}-QC}hU#{hjU=QaZKo zo|c2Xy|!70%~?#l&zgLXi|3$6k)(qhVi+2138KsS=k*@8dhWyVnZsvo;mf7N{|!3I=QK~P_qTjP3CjV zX!GZ2%`z)TGnCVT?r4!!u`MDFPxlG?yUOoe(;u0YcCbug2Q}-YTc*f^HtCBoDd8gY zfuIxaH6f+;)tJ7gr*_V zgivc@#IC~Zmo-?;8zSZSvr}$0p{7+g5Bg4ODr{-G$R^lBYpAl&?ChA*;m@2s1vlZ^!jRjQE^;YH*Y!M8 zd%0)KpVr4BvgBE$qEzfU^VFap=3%)VRK2N|Fu84v1I0ya&4mWDMOAXK`V7c0eMGU# zIew#nZm$A|hkdCr9I?56$=?K0&$N}%kae|7%1pS#7n(MWpsfARi!rKX|sJ_s%})% zqcJzv-UO2~!uNVwYxK*ZS{q0snMTJ_O+Vpfi`ukvQ*NhV;xjJawnLal_VSZ%My0JX zL#B5^SVyt2h1#itvaX$Sg#cd#&s2G%q51rcsR{?ug%m%q<^J5`wd!GfyomF`fS~`DXz-E5Za{I#CP<1J3{3!A6^zQ28EkEg3q%o%m5xBujN&}2D z8(*L2fH@9N;-Drx!bo!ATGt+I4}?9QTTf^iG$)29EPCQr49XTuptk-!(Dl!wi23pL z>z}@6xWCw_ycRU<=G)$}_6E6Tv58_VtJUyiU+b)387?H^zDWT~C=4v&I~QtV7Z%yQ z=D%A26&-ZA-I2rcApVq0qXg1lWku_V8H!l{6um z;?B*So^rN|AdwM=gJ-+y## z&<{d&jI&EhUS{*nCkmlv@77|iOH07@lD=W&65}SDCtiPQ|J=KM{U<9Hwe)9gf1Fl9 z5brpHo63Ji-aoSS-m^YCA4XO|$R$|m5bP4zz-VuF~S!Cnxt9F4R?VZbZ( z;Ri+XHem;}aKXQBiWLB%JHCI#qenm_xIwlK`q#HQRJR0ZwEtHqE5B`EKo5HRn~zzv zH=;>NUEoxM>x;(7uv*^)n_#f)0#&7pTJfDcEoQg~)s4w3 zT_Hu|=WT%qdhO)e*RRpYp6(QBm`9p3jlB9r`nK{2mTaHz5}G^faUPzY4E_N>M$mR+!#y~tHYg5CfS-+y1z3l zDcbDytkxy)3hdAo5~`qK0*Dj(~P3$qs0h8SVft5g|*Lc8oRE)AoS&F)l+QE~jl z>x*u?{rcC)4aeP__WbPwmSkl;X72cA+-miVh>XmE%0HdW7sj+kvc}E!=h^`Q`dJt- zP}(tvCr!65tCI$xwIz2SaRgHK{OtY4>1M@|Kc1gaj=w@rNLPE4Wi!BMzrSHTD}EFG zurW>IryhDA%|-Ch*pqe7DKx3PbNaH(o%Kqs%-SWm5xIY~PFD1EpHusUXO-{d17;uv znK-YB*r+Z|e}B5AV*Flc7nzb8z9FR8;@4gE^~PM+*2koCBuom=OYUkF?#j$JjGZ5S zM)$ru#1@vov%<=$W$kS{G%Wn@CJLBv_-!u4U@(~Xt@oKtBFZq4BYqlUwq1eXeNI-W z8t}xCUvYJ9D=Z|J)f6h0N9V}_ zuw{TaA`cy$<}-`Gc+j7iy_FaI$3j)~ojgkpbJo@6I6>uofqe_4x}MTn_+^p1c<6$}-p0;atVz|q?}`~`jb(b zlw-v+^!H!Samva+c#wr{cAZ#hoowzD)*e;$87X%fLs7PM%u=|}yG9RkbmO>@gd9_( z_2(q}3Ir4fJH={jj`lZYW@Z-qEw#IajwP++APtXcYb=!bBu6u7@XZRF>Y3);4|{Rf zD46;lF~oVK`hGy*t|!^SJk61S#*xA-gy=Rj>3cJ|$M}^gI+1LOd^j9#b}=}zVEsZZ zK8BU{iO^x6x0@FF9?dE-UlBH+M&(>K)Zy4rFP@sR9j*(k^YHN4D)&iKf<5qB7`hUA zhJ91b^rE=4i;GK354^;`^;#F`j*r&)sfR$c%)^>?L?@h10mm|^qW(fnP* z=~TS@0POH`C8C*-!)yhbX=BAcUp7J5f%vuF4!f^2}0!Ey|m^vL7k!syOL zhDAR5xt1TIrj0CM_R-UC3r~n!W_~o8(~%xToN{mbJoQ6#_+NK@HK~>k%+}LwZPn9_ zBC^QXuzHt>+L`Jjj_{v5ypC`6ImLoJI$64oIENnee0N^|h-7K<3igCq2Ahh0+)#CMZWu*zc4ds_@gHncz+Ud zxvbcjncX(4eAaTJiu;|cT0cDeZMfVuQ$jz5J1iq0wzyb`AXSn+*_llVX3X*=TFlPQ zt~l}Af(Hl$VQJ^l<=yaYF!nf!F3#bwL!LenNad58vF%wB`*;BwZ=v|P&jU);dKI%u z)j(*yMKz4Jk4xR%g3j7MiblR+)b7MWI+HfN2o9T{K5G(;A?=ekofFkz?pQyf=EEjx zS??wA0XlBmNnO-+yNO}a38x&TMqYgEOtF94lq05g?%@0X+V0w?x^g{ z8x@EQg?b$Ph$BiT=_*be-a)>mZT(}dCwo6LYNBm!0G;Gc=tzhKm)(dfhM&{dTOzcwPV|br6>#wTbZAZO5Rvt1*MU6oGbYnL*VkLS9Ie5Bt$Zc>BZ=%;G9UG*pavSDn*R>6{c~f@SAGcuF()m?xLR(QDXiioLFq%ej8_50A6AEaU{)~GCmKmbR*sY$Tb97EP(qL z-N{Nn^;2Id=FJ(UnK!G^y>{zL8}X@IOErao(_jF%6dkM`fW(7*ta&rR> zjF>H#fb*VNzPUOv`v_iXmgACNdzAR= zip;z_r2`|v!`r61Q^WyS%D-RmdlyyUKq1Lb{#CTfn+V~KR*0|Wn5Spi~3E2bPI7(k|%kdE)3HmC#N z^{;RHxAhN+16HV)1!Xvv;=(2BnNL6I)Pm>9h_@9_8LYDr z@&adPfTip(so^K=+{-ij$yz|lK_4d2wWQ%TwKfa63*)lW(0;R?EEB1)SDE^hY!jBk z>oQWVTk8$Uuy=@St{`Z2ezNbg-W-u|NGy~RUu1AEtkHirp5F-<)>ctbQGbRUuC&Tr zO8{oeeclw25MpH4;QLx`AsGq8scL)CmO^6~rx$R8W+9JG0*@ z*x z9s3=GPav)44|ntk23K8pF@7|dl|J}mY3(nPBy<>VXGAjB*V&H)SY9jmZ`GGX`|$gk z_wTxe-CQpUR2tNRxYDBxs63X1-8e;&8Js4s;gZJTMC887>|Hru68~9{6c&xsM+Ch z%fs)N+{c(5B%*MpuFC~%j!fFBX_(GW@?LQy;_)?DVb$kLuQV`2KA(R?uUznrsi)h zQN3LNz`7_}M1DTM#kOs#3X{6=pcJWJEFzy<)*UYI=?GkJMZ2gyN*IyfNH377?gNL(A;MX>3 zS{5Sf=t~CzX#rFCa}cCja&j^Vbg^yZMQu~9O=n{4iAeejhsoILDS7TwR?w<-b~{T; zfQ8=#GPdcSj3~o@DDpoP`5%h>4@EvmP5zuge}Rv3;{(HP zR|TS{e01!Zm%6;)WBJ#I9HWlJUk0Yy5MeIj%hZGGZ*O2iVm>H}{{WX5&y)_cQSr() z(#ETp(4yvXPv>-olN23pqbQMSb8I8U*i0xCiT{!3NcvXkk}A;ZOKRV|bff)X)X|}1 znbSVS4IVCE^TQk-;aKWhiWMp4HPSv+B{2_(bz}$s)UQw!#oCYd?F8WN9M?uLx!*4o z^5Ai%*q|Fhy9Sb`Agp}`3QY~Rw_|@-<6sgaIG&+BsTi*?T~Lzo=V8i zIwYZNjgG%dGX}9DXXxl;DWkPM{TkTW@{dqM(dUjMm3NbKb>o`#l{Jlx;Vvo0J*$&_ zyNxt`)!rrR>+4(P7sZR`h(tJxwDVSgI;?p9B_*U?@=)ucw?_zrD>>S&?8INdjEZ76A%O`>^b`Etqnh+f z6BMHMYMS>%hPYkF>tLG6`sY+?Q3Vnm^8xk=%H6vu^y3`3L>&uwfpKv*Ad1}(0;pIC z%l!2@OMiJG&$iUMo^}&rAG8*mN<){02|ts<61tpAYcPFI1wuxu7jOtsBaHqDBWHJql1g zdzy-`*K~fTcMvbXuBaZ>Ep5nm={5@JqfpfBK&H_#c-6rDw1p#yV|+Z-0pQ2EzdY zQ~NNO7O@qHL0p077uZbv47Hg^_p{zYQzq*B^*j5#2?@9wZ+QxJl&~#$7mX6Pg%y{n znI85R$6)Jy5|d7IGT{_;-+=2*rAIO^q?TmCgr+z%Y0d^toHJE+Nq3ZN4FG_k)2kYF0Jixo@l4YMie=c(& zC`}XJ<2XBjwN|dt&q8So0(pX)=dBXU9qiZdcF7_yeB+WLT@%SdT05i zN2=_&yag~QUKefI@FE;!hor!9e}HCBr{@}6ojgW$CR}5|?%jUQ!@(-x=` zpAzwsdB!dyfiPH9GnrDSuC8vqHr0JV%?I)ae(wTYtPE#R;uNO zSJ!%0iINTz6XHE>N9ZRNTlB^uJ<7si3H;f(wUbtWz?-o-@cnx-pqShCr2nCqYv~iG z*76|5ynqA$XwScD{i_P}t`u?#lO&#P)q8dLvjco4!h&z5TRIjYHxRZe1trGmYax?# z=Luqr^2L4nLMh^Qdg?()*^%B^e?a&>w17yQvTNk^k`GF;$jodtU){?ZY=L0C+MTjy z-S`Tp5pH}7o#{OOKKaXBze7&9mM5+bsd=OglE5dcLmS@97jL) z{>j(1R0@!mifXK0-KN-CDICuwoBNMAHk@b5i_9iTSO~msaG-s2aRinzGud}1L*ks` zyp@cu)S6fzQh9{%EY$M8?F zavCU<_?zpsSNUN-P2(# zw(T>9AL|8*J3#OY;Z}yOTwV1}0JEEnKi8xlcK=IOZ;iev09_+jyKcxHdx&@x9-%Bf z@=!9*U{o^CM4r;h#ZaJG5>*q0%57-6I$uoVyjDc2+Z5b?0eWj0#Kg-J@s57r0-e*X zifdfX?=L&($WX40aVM6xeA|;ekKM>?T~acpx=Vu8ctMmk5Q-&mwtn21`Yeuv(KwTw zP}1p3+m1j9&{}b)7r1<5$^F+<;{^lA&Eg|URwK1Q@dMlmMY;UzB%27r(cCiJF3&0Z zq4^wa1kO^*t7FvN+^oV6>(0l*O9SA%fyoWUkrtP|QzxC5hr-H_4a)X<7RzIA3rx|u zNBCU6L|+e-MEviThy0Z=|0|;Y2S5I=s!RT%*8jNA|G3ZpxX=H%&;MV$&%7^Hq2PxR zVfEERL)E`n_Oc_5`SBl-s)*-hj{uuFqM1H#ni-#ukf@FVl-OprkLLk0`=u+oAXS8$ z%fi6Mwli-di0Q*40BRFB35&NOw4Z+r`3!sFgq*6#I=f~=2-ev(bT#s3acztob+K>a)Xj`Et z)9p)~KgurOI-BR-ljjCY#pje{BRA{wJc;wv#i3NZtN^q|w3KypSs1+Yz*1f?m0=Vj zlU{AVU>!;)KV9(fNs!gE>pThhWDteJY9DImBG-X&xH0w%->y4OOCbH^Mp$%=ea+`` zM7^o!i_fS_EtIZ}Dt=)_h|Z6HG#F?Y|Hb2Py6mh5e|4uaF`@0BZ_(4N$xWqS`9^2m z&}OT!i9ePu=d_qYm)jDhf7bsnU5FF962bjpcD_nTJcU)h`|^2}9=V88zwcG*`u4}E zk|#@vGCcK5be4ILvmQHNQ|j)O9J9Rl?{caCS%vf8v#x)C5NC(*oX(5*HBxfj)3l zT$oy$?$O{g{$P0RSGCqz+or)^MsaMA&!b0e!_$DbcHm0~vu3_NX+6>s(Wl^)%oVV$ zSzTT2KT_+PC=6d>w1^%=`DN$kwpJ7&T<-@4){R!WHj%{V+|AiXrFDLHo^Ln#|7<*_ z$sO0X@pe_Zg&8Z;rR4r%FDJEZepEuAH*%Rh2GGwt- zTGet`4RVzR8Da!ABp7C#n|LdH8uoL6CEWepa|JP**B-*=eA94&nCCsp@VzLdSk3}t ziY*2P_QZLJS@E%A3)#@4={iUq69s`Wi(ALQI_1S)L z;QrUE0s~=QNLE^1Ih{$F6UAeBC8FzK`>0Q-m5}u$)8+b5_KxTM z0u{g@H=t(87iXe>_xSjTYo5x~4ot7b7tS`0ngXnRPh0pG|a%yOXaOcMJIR@RzjRqn}KZ_qHA70?M6UzI>@?DcMM`Cj095%V19VxaCygukJ>61u}_LBpa4_NZWk>jn|{i@_K6scnw=iPZ*Yo9Z!Z5yZr zD3BEr0BliL+zG!D(fPHHm;4TSy|)e`RGF+Qq_p)`xI;xfqnGl{z-_FCShknu4it`& z3S)p$tppflQ#O!sv6w~$cblmQ9q5~C5n16@B8lgXcmI6FQ)6C;0FYUPbGtc zgMd&V?zic9AW&#?SsD5IN@wWrx~Z)k;Z#|h?#vElV}5gB#A8yg)4gG6FE+1Hby>d` z=7NrC{+6c~U%}*;;eRi=+wsAEV%3mqLV(+Fs;s-%)zt3cNWE!(Aa3b!m5J1_wCnsLJ}{e`Aw-@87U zb(JnD0V0AB1T8>EM|Yq|jE6-0S=eU8{2=WZpi+Hwz#P`kH1HPOqRR7Dsc^#a)z$4) z?|d%SD|aC*QB;NDA=_DDGk57%5;9=i8c}QsOG``Zo3~o;`<}8Oww#^g#lQ1tbuS^76J7=QD< zs8DH(s-<~Xp+o#r-30O+VgKt?9QCmV z;>jDDXKmjKFkiF1l^pqwz5yYT+#2>Y;#)ISuNU@M(}I#$dX$5lJP|ArsV76fXX>VY$J3857Vl*Bv->mdr;j4hcQ@c+1T+fCM)UR?0MzOc zrQh-%O23M{A%pxPC6O-IuiNd2{Bj=~+8qF!H>eY6X1R8bR_0mkfmR?Z+Lem()wiSQ zIr%|ZnDO-s-%;wRIk_cGkp}4>_!T9ADmhk0u_o#}=Yh0^h3*|jOY-e=CP%q+A541x z`(#ndY{WxoE>67S$Sc6-?`W2dNRtaLLXIu>4?nN;%?J&a#(HURYzh%2r)ZxKOFWCc z0*qSet(J?;E}r$><7$@;cKU6uM#b;_a?Vl*CV)OER6c+YvpsEwH<*Lg@rJW^hBF}) zf|w8>V4;%^!C+UEPg_j+IFf}>cI_k0x40%Tp-96<&i%ZBS(CZz*FT_BaYI>^0w=7@$ E54FnyY5)KL literal 7869 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 literal 0 HcmV?d00001 diff --git a/ProgramScreenshots/SettingsSiteXHamster.png b/ProgramScreenshots/SettingsSiteXHamster.png new file mode 100644 index 0000000000000000000000000000000000000000..1365fadfa5bd846777d349f1680731e1891af135 GIT binary patch literal 14196 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 UWw|Jdf&YMXG!4NO>JP*I50%6lg#Z8m literal 0 HcmV?d00001 diff --git a/ProgramScreenshots/SettingsSiteXvideos.png b/ProgramScreenshots/SettingsSiteXvideos.png index 7368dfce3d294441d2a027d2431f4df003e5339f..87262eb73f0cfc0d7bb4183ef667887f192a110d 100644 GIT binary patch literal 15488 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 As{jB1 literal 14780 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 ''' will take precedence if it is defined. ''' - Public Sub New(Optional ByVal JobsCount As Integer = -1) - TasksCount = JobsCount + Public Sub New(Optional ByVal TasksCount As Integer = -1) + Me.TasksCount = TasksCount End Sub End Class ''' A property attribute that specifies how many users should be downloaded at the same time in one thread Public NotInheritable Class TaskCounter : Inherits Attribute End Class + ''' + ''' This attribute cannot be combined with . + ''' If set to , this attribute will be ignored + ''' + ''' + Public NotInheritable Class TaskGroup : Inherits Attribute + Public ReadOnly Name As String + ''' Initialize a new TaskGroup attribute. + ''' Group name + Public Sub New(ByVal Name As String) + Me.Name = Name + End Sub + End Class ''' This attribute indicates that the plugin has a SavedPosts environment Public NotInheritable Class SavedPosts : Inherits Attribute End Class diff --git a/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb b/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb index 681982f..0a38d14 100644 --- a/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb +++ b/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb @@ -17,9 +17,9 @@ Namespace Plugin Property ID As String Property ParseUserMediaOnly As Boolean Property UserDescription As String - Property ExistingContentList As List(Of PluginUserMedia) + Property ExistingContentList As List(Of IUserMedia) Property TempPostsList As List(Of String) - Property TempMediaList As List(Of PluginUserMedia) + Property TempMediaList As List(Of IUserMedia) Property UserExists As Boolean Property UserSuspended As Boolean Property IsSavedPosts As Boolean diff --git a/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb b/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb index e109492..4164e96 100644 --- a/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb +++ b/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb @@ -18,12 +18,12 @@ Namespace Plugin ReadOnly Property Image As Image ReadOnly Property Site As String Property Logger As ILogProvider - Function GetUserUrl(ByVal UserName As String, ByVal Channel As Boolean) As String + Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String Function IsMyUser(ByVal UserURL As String) As ExchangeOptions Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable Function GetInstance(ByVal What As Download) As IPluginContentProvider - Function GetUserPostUrl(ByVal UserID As String, ByVal PostID As String) As String + Function GetUserPostUrl(ByVal User As IPluginContentProvider, ByVal Media As IUserMedia) As String #Region "XML Support" Sub Load(ByVal XMLValues As IEnumerable(Of KeyValuePair(Of String, String))) #End Region diff --git a/SCrawler.PluginProvider/My Project/AssemblyInfo.vb b/SCrawler.PluginProvider/My Project/AssemblyInfo.vb index 2855725..5dafee7 100644 --- a/SCrawler.PluginProvider/My Project/AssemblyInfo.vb +++ b/SCrawler.PluginProvider/My Project/AssemblyInfo.vb @@ -32,6 +32,6 @@ Imports System.Runtime.InteropServices ' by using the '*' as shown below: ' - - + + diff --git a/SCrawler.PluginProvider/Objects/PluginUserMedia.vb b/SCrawler.PluginProvider/Objects/PluginUserMedia.vb index 7e9a114..d5693e0 100644 --- a/SCrawler.PluginProvider/Objects/PluginUserMedia.vb +++ b/SCrawler.PluginProvider/Objects/PluginUserMedia.vb @@ -7,25 +7,44 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Namespace Plugin - Public Structure PluginUserMedia - Enum Types As Integer - Undefined = 0 - [Picture] = 1 - [Video] = 2 - [Text] = 3 - VideoPre = 10 - GIF = 50 - m3u8 = 100 - End Enum - Enum States As Integer : Unknown = 0 : Tried = 1 : Downloaded = 2 : Skipped = 3 : Missing = 4 : End Enum - Public ContentType As Integer - Public URL As String - Public MD5 As String - Public File As String - Public DownloadState As Integer - Public PostID As String - Public PostDate As Date? - Public SpecialFolder As String - Public Attempts As Integer + Public Enum UserMediaTypes As Integer + Undefined = 0 + [Picture] = 1 + [Video] = 2 + [Text] = 3 + VideoPre = 10 + GIF = 50 + m3u8 = 100 + End Enum + Public Enum UserMediaStates As Integer + Unknown = 0 + Tried = 1 + Downloaded = 2 + Skipped = 3 + Missing = 4 + End Enum + Public Structure PluginUserMedia : Implements IUserMedia + Public Property ContentType As Integer Implements IUserMedia.ContentType + Public Property URL As String Implements IUserMedia.URL + Public Property URL_BASE As String Implements IUserMedia.URL_BASE + Public Property MD5 As String Implements IUserMedia.MD5 + Public Property File As String Implements IUserMedia.File + Public Property DownloadState As Integer Implements IUserMedia.DownloadState + Public Property PostID As String Implements IUserMedia.PostID + Public Property PostDate As Date? Implements IUserMedia.PostDate + Public Property SpecialFolder As String Implements IUserMedia.SpecialFolder + Public Property Attempts As Integer Implements IUserMedia.Attempts End Structure + Public Interface IUserMedia + Property ContentType As Integer + Property URL As String + Property URL_BASE As String + Property MD5 As String + Property File As String + Property DownloadState As Integer + Property PostID As String + Property PostDate As Date? + Property SpecialFolder As String + Property Attempts As Integer + End Interface End Namespace \ No newline at end of file diff --git a/SCrawler/API/Base/M3U8Base.vb b/SCrawler/API/Base/M3U8Base.vb new file mode 100644 index 0000000..a9b0809 --- /dev/null +++ b/SCrawler/API/Base/M3U8Base.vb @@ -0,0 +1,64 @@ +' 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 PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web +Imports PersonalUtilities.Tools.Web.Clients +Namespace API.Base + Namespace M3U8Declarations + Friend Module M3U8Defaults + Friend ReadOnly TsFilesRegEx As RParams = RParams.DM(".+?\.ts[^\r\n]*", 0, RegexReturn.List) + End Module + End Namespace + Friend NotInheritable Class M3U8Base + Private Sub New() + End Sub + Friend Shared Function CreateUrl(ByVal Appender As String, ByVal File As String) As String + File = File.StringTrimStart("/") + If File.StartsWith("http") Then + Return File + Else + If File.StartsWith("hls/") And Appender.Contains("hls/") Then _ + Appender = LinkFormatterSecure(Appender.Replace("https://", String.Empty).Split("/").First) + Return $"{Appender.StringTrimEnd("/")}/{File}" + End If + End Function + Friend Shared Function Download(ByVal URLs As List(Of String), ByVal DestinationFile As SFile, Optional ByVal Responser As Response = Nothing) As SFile + Dim CachePath As SFile = Nothing + Try + If URLs.ListExists Then + Dim ConcatFile As SFile = DestinationFile + If ConcatFile.Name.IsEmptyString Then ConcatFile.Name = "PlayListFile" + ConcatFile.Extension = "mp4" + CachePath = $"{DestinationFile.PathWithSeparator}_Cache\{SFile.GetDirectories($"{DestinationFile.PathWithSeparator}_Cache\",,, EDP.ReturnValue).ListIfNothing.Count + 1}\" + If CachePath.Exists(SFO.Path) Then + Dim p As New SFileNumbers(ConcatFile.Name,,, New ANumbers With {.Format = ANumbers.Formats.General}) + ConcatFile = SFile.Indexed_IndexFile(ConcatFile,, p, EDP.ReturnValue) + Dim i% + Dim eFiles As New List(Of SFile) + Dim dFile As SFile = CachePath + dFile.Extension = "ts" + Using w As New DownloadObjects.WebClient2(Responser) + For i = 0 To URLs.Count - 1 + dFile.Name = $"ConPart_{i}" + w.DownloadFile(URLs(i), dFile) + eFiles.Add(dFile) + Next + End Using + DestinationFile = FFMPEG.ConcatenateFiles(eFiles, Settings.FfmpegFile, ConcatFile, p, EDP.ThrowException) + eFiles.Clear() + Return DestinationFile + End If + End If + Return Nothing + Finally + CachePath.Delete(SFO.Path, SFODelete.None, EDP.None) + End Try + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Base/ProfileSaved.vb b/SCrawler/API/Base/ProfileSaved.vb index 15211e3..71e49b0 100644 --- a/SCrawler/API/Base/ProfileSaved.vb +++ b/SCrawler/API/Base/ProfileSaved.vb @@ -7,8 +7,8 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Threading -Imports PersonalUtilities.Forms.Toolbars Imports SCrawler.Plugin.Hosts +Imports PersonalUtilities.Forms.Toolbars Imports PDownload = SCrawler.Plugin.ISiteSettings.Download Namespace API.Base Friend NotInheritable Class ProfileSaved @@ -52,6 +52,7 @@ Namespace API.Base ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[API.Base.ProfileSaved.Download({HOST.Key})]") Finally HOST.DownloadDone(PDownload.SavedPosts) + MainFrameObj.UpdateLogButton() End Try End Sub End Class diff --git a/SCrawler/API/Base/SiteSettingsBase.vb b/SCrawler/API/Base/SiteSettingsBase.vb index 0d289ce..a9cbf3a 100644 --- a/SCrawler/API/Base/SiteSettingsBase.vb +++ b/SCrawler/API/Base/SiteSettingsBase.vb @@ -7,7 +7,8 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Cookies Imports SCrawler.Plugin Imports Download = SCrawler.Plugin.ISiteSettings.Download Namespace API.Base @@ -40,6 +41,7 @@ Namespace API.Base .CookiesEncryptKey = SettingsCLS.CookieEncryptKey .SaveSettings() End If + If .CookiesDomain.IsEmptyString Then .CookiesDomain = CookiesDomain End With End Sub #Region "XML" @@ -74,15 +76,18 @@ Namespace API.Base #Region "User info" Protected UrlPatternUser As String = String.Empty Protected UrlPatternChannel As String = String.Empty - Friend Overridable Function GetUserUrl(ByVal UserName As String, ByVal Channel As Boolean) As String Implements ISiteSettings.GetUserUrl + Friend Overridable Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String Implements ISiteSettings.GetUserUrl If Channel Then - If Not UrlPatternChannel.IsEmptyString Then Return String.Format(UrlPatternChannel, UserName) + If Not UrlPatternChannel.IsEmptyString Then Return String.Format(UrlPatternChannel, User.Name) Else - If Not UrlPatternUser.IsEmptyString Then Return String.Format(UrlPatternUser, UserName) + If Not UrlPatternUser.IsEmptyString Then Return String.Format(UrlPatternUser, User.Name) End If Return String.Empty End Function - Friend Overridable Function GetUserPostUrl(ByVal UserID As String, ByVal PostID As String) As String Implements ISiteSettings.GetUserPostUrl + Private Function ISiteSettings_GetUserPostUrl(ByVal User As IPluginContentProvider, ByVal Media As IUserMedia) As String Implements ISiteSettings.GetUserPostUrl + Return GetUserPostUrl(User, Media) + End Function + Friend Overridable Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String Return String.Empty End Function Protected UserRegex As RParams = Nothing @@ -94,7 +99,7 @@ Namespace API.Base End If Return Nothing Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[API.Base.SiteSettingsBase.IsMyUser({UserURL})]") + Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[API.Base.SiteSettingsBase.IsMyUser({UserURL})]", New ExchangeOptions) End Try End Function Protected ImageVideoContains As String = String.Empty diff --git a/SCrawler/API/Base/Structures.vb b/SCrawler/API/Base/Structures.vb index b90c9ce..ba3b061 100644 --- a/SCrawler/API/Base/Structures.vb +++ b/SCrawler/API/Base/Structures.vb @@ -6,12 +6,13 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY +Imports SCrawler.Plugin Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.XML.Base Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Base Friend Module Structures - Friend Structure UserMedia : Implements IEquatable(Of UserMedia), IEContainerProvider + Friend Structure UserMedia : Implements IUserMedia, IEquatable(Of UserMedia), IEContainerProvider #Region "XML Names" Friend Const Name_MediaNode As String = "MediaData" Private Const Name_MediaType As String = "Type" @@ -48,6 +49,89 @@ Namespace API.Base ''' SomeFolder\SomeFolder2 ''' Friend SpecialFolder As String + Friend [Object] As Object +#Region "Interface Support" + Private Property IUserMedia_Type As Integer Implements IUserMedia.ContentType + Get + Return Type + End Get + Set(ByVal Type As Integer) + Me.Type = Type + End Set + End Property + Private Property IUserMedia_URL_BASE As String Implements IUserMedia.URL_BASE + Get + Return URL_BASE + End Get + Set(ByVal URL_BASE As String) + Me.URL_BASE = URL_BASE + End Set + End Property + Private Property IUserMedia_URL As String Implements IUserMedia.URL + Get + Return URL + End Get + Set(ByVal URL As String) + Me.URL = URL + End Set + End Property + Private Property IUserMedia_MD5 As String Implements IUserMedia.MD5 + Get + Return MD5 + End Get + Set(ByVal MD5 As String) + Me.MD5 = MD5 + End Set + End Property + Private Property IUserMedia_File As String Implements IUserMedia.File + Get + Return File + End Get + Set(ByVal File As String) + Me.File = File + End Set + End Property + Private Property IUserMedia_State As Integer Implements IUserMedia.DownloadState + Get + Return State + End Get + Set(ByVal State As Integer) + Me.State = State + End Set + End Property + Private Property IUserMedia_PostID As String Implements IUserMedia.PostID + Get + Return Post.ID + End Get + Set(ByVal PostID As String) + Post.ID = PostID + End Set + End Property + Private Property IUserMedia_PostDate As Date? Implements IUserMedia.PostDate + Get + Return Post.Date + End Get + Set(ByVal PostDate As Date?) + Post.Date = PostDate + End Set + End Property + Private Property IUserMedia_SpecialFolder As String Implements IUserMedia.SpecialFolder + Get + Return SpecialFolder + End Get + Set(ByVal SpecialFolder As String) + Me.SpecialFolder = SpecialFolder + End Set + End Property + Private Property IUserMedia_Attempts As Integer Implements IUserMedia.Attempts + Get + Return Attempts + End Get + Set(ByVal Attempts As Integer) + Me.Attempts = Attempts + End Set + End Property +#End Region Friend Sub New(ByVal URL As String) Me.URL = URL URL_BASE = URL @@ -58,10 +142,10 @@ Namespace API.Base Me.New(URL) Me.Type = Type End Sub - Friend Sub New(ByVal m As Plugin.PluginUserMedia) + Friend Sub New(ByVal m As Plugin.IUserMedia) [Type] = m.ContentType URL = m.URL - URL_BASE = URL + URL_BASE = m.URL_BASE MD5 = m.MD5 File = m.File Post = New UserPost With {.ID = m.PostID, .[Date] = m.PostDate} @@ -117,19 +201,6 @@ Namespace API.Base Public Overrides Function ToString() As String Return URL End Function - Friend Function PluginUserMedia() As Plugin.PluginUserMedia - Return New Plugin.PluginUserMedia With { - .ContentType = Type, - .DownloadState = State, - .File = File, - .MD5 = MD5, - .URL = URL, - .SpecialFolder = SpecialFolder, - .PostID = Post.ID, - .PostDate = Post.Date, - .Attempts = Attempts - } - End Function Friend Overloads Function Equals(ByVal Other As UserMedia) As Boolean Implements IEquatable(Of UserMedia).Equals Return URL = Other.URL End Function @@ -154,17 +225,31 @@ Namespace API.Base ''' Post ID Friend ID As String Friend [Date] As Date? -#Region "Channel compatible fields" Friend UserID As String Friend CachedFile As SFile +#Region "Initializers" + Public Sub New(ByVal ID As String) + Me.ID = ID + End Sub + Public Sub New(ByVal [Date] As Date?) + Me.Date = [Date] + End Sub + Public Sub New(ByVal ID As String, ByVal [Date] As Date?) + Me.ID = ID + Me.Date = [Date] + End Sub + Public Shared Widening Operator CType(ByVal ID As String) As UserPost + Return New UserPost(ID) + End Operator + Public Shared Widening Operator CType(ByVal Post As UserPost) As String + Return Post.ID + End Operator #End Region - Friend Function GetImage(ByVal s As Size, ByVal e As ErrorsDescriber, ByVal NullArg As Image) As Image - If Not CachedFile.IsEmptyString Then - Return If(PersonalUtilities.Tools.ImageRenderer.GetImage(SFile.GetBytes(CachedFile), s, e), NullArg.Clone) - Else - Return NullArg.Clone - End If +#Region "ToString" + Public Overrides Function ToString() As String + Return ID End Function +#End Region #Region "IEquatable, IComparable Support" Friend Overloads Function Equals(ByVal Other As UserPost) As Boolean Implements IEquatable(Of UserPost).Equals Return ID = Other.ID diff --git a/SCrawler/API/Base/UserDataBase.vb b/SCrawler/API/Base/UserDataBase.vb index 14217c8..ca7a67a 100644 --- a/SCrawler/API/Base/UserDataBase.vb +++ b/SCrawler/API/Base/UserDataBase.vb @@ -10,7 +10,7 @@ Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Forms.Toolbars Imports PersonalUtilities.Tools -Imports PersonalUtilities.Tools.WEB +Imports PersonalUtilities.Tools.Web.Clients Imports System.IO Imports System.Net Imports System.Threading @@ -94,9 +94,16 @@ Namespace API.Base End Sub #End Region #Region "XML Declarations" - Private Const Name_Site As String = "Site" - Private Const Name_IsChannel As String = "IsChannel" - Private Const Name_UserName As String = "UserName" + Private Const Name_Site As String = UserInfo.Name_Site + Private Const Name_Plugin As String = UserInfo.Name_Plugin + Private Const Name_IsChannel As String = UserInfo.Name_IsChannel + Friend Const Name_UserName As String = "UserName" + Private Const Name_Model_User As String = UserInfo.Name_Model_User + Private Const Name_Model_Collection As String = UserInfo.Name_Model_Collection + Private Const Name_Merged As String = UserInfo.Name_Merged + Private Const Name_SpecialPath As String = UserInfo.Name_SpecialPath + Private Const Name_SpecialCollectionPath As String = UserInfo.Name_SpecialCollectionPath + Private Const Name_UserExists As String = "UserExists" Private Const Name_UserSuspended As String = "UserSuspended" Private Const Name_FriendlyName As String = "FriendlyName" @@ -108,8 +115,8 @@ Namespace API.Base Private Const Name_CreatedByChannel As String = "CreatedByChannel" Private Const Name_SeparateVideoFolder As String = "SeparateVideoFolder" - Private Const Name_CollectionName As String = "Collection" - Private Const Name_LabelsName As String = "Labels" + Private Const Name_CollectionName As String = UserInfo.Name_Collection + Friend Const Name_LabelsName As String = "Labels" Private Const Name_ReadyForDownload As String = "ReadyForDownload" Private Const Name_DownloadImages As String = "DownloadImages" @@ -122,7 +129,7 @@ Namespace API.Base Private Const Name_ScriptUse As String = "ScriptUse" Private Const Name_ScriptData As String = "ScriptData" - Private Const Name_DataMerging As String = "DataMerging" + Friend Const Name_DataMerging As String = "DataMerging" #End Region #Region "Declarations" #Region "Host, Site, Progress, Self" @@ -156,6 +163,21 @@ Namespace API.Base End Property Friend Overridable Property ID As String = String.Empty Implements IContentProvider.ID, IPluginContentProvider.ID Friend Overridable Property FriendlyName As String = String.Empty Implements IContentProvider.FriendlyName + Friend ReadOnly Property UserModel As UsageModel Implements IUserData.UserModel + Get + Return User.UserModel + End Get + End Property + Friend Overridable ReadOnly Property CollectionModel As UsageModel Implements IUserData.CollectionModel + Get + Return User.CollectionModel + End Get + End Property + Friend Overridable ReadOnly Property IsVirtual As Boolean Implements IUserData.IsVirtual + Get + Return UserModel = UsageModel.Virtual + End Get + End Property #End Region #Region "Description" Friend Property UserDescription As String = String.Empty Implements IContentProvider.Description, IPluginContentProvider.UserDescription @@ -231,7 +253,7 @@ Namespace API.Base Protected Function GetNullPicture(ByVal MaxHeigh As XML.Base.XMLValue(Of Integer)) As Bitmap Return New Bitmap(CInt(DivideWithZeroChecking(MaxHeigh.Value, 100) * 75), MaxHeigh.Value) End Function - Protected Function GetPicture(Of T)(Optional ByVal ReturnNullImageOnNothing As Boolean = True, Optional ByVal GetToast As Boolean = False) As T + Friend Function GetPicture(Of T)(Optional ByVal ReturnNullImageOnNothing As Boolean = True, Optional ByVal GetToast As Boolean = False) As T Dim rsfile As Boolean = GetType(T) Is GetType(SFile) Dim f As SFile = Nothing Dim p As UserImage = Nothing @@ -335,7 +357,7 @@ BlockNullPicture: Friend Overridable Sub ChangeCollectionName(ByVal NewName As String, ByVal UpdateSettings As Boolean) Dim u As UserInfo = User u.CollectionName = NewName - u.IncludedInCollection = Not NewName.IsEmptyString + u.UpdateUserFile() User = u If UpdateSettings Then Settings.UpdateUsersList(User) End Sub @@ -455,7 +477,10 @@ BlockNullPicture: End Property Friend Overridable Function GetUserInformation() As String Dim OutStr$ = $"User: {Name} (site: {Site}" - If IncludedInCollection Then OutStr &= $"; collection: {CollectionName}" + If IncludedInCollection Then + OutStr &= $"; collection: {CollectionName}" + If CollectionModel = UsageModel.Default And UserModel = UsageModel.Virtual Then OutStr &= "; virtual" + End If OutStr &= ")" OutStr.StringAppendLine($"Labels: {Labels.ListToString}") OutStr.StringAppendLine($"Path: {MyFile.CutPath.Path}") @@ -494,9 +519,9 @@ BlockNullPicture: Private Property IPluginContentProvider_Thrower As IThrower Implements IPluginContentProvider.Thrower Private Property IPluginContentProvider_LogProvider As ILogProvider Implements IPluginContentProvider.LogProvider Friend Property ExternalPlugin As IPluginContentProvider - Private Property IPluginContentProvider_ExistingContentList As List(Of PluginUserMedia) Implements IPluginContentProvider.ExistingContentList + Private Property IPluginContentProvider_ExistingContentList As List(Of IUserMedia) Implements IPluginContentProvider.ExistingContentList Private Property IPluginContentProvider_TempPostsList As List(Of String) Implements IPluginContentProvider.TempPostsList - Private Property IPluginContentProvider_TempMediaList As List(Of PluginUserMedia) Implements IPluginContentProvider.TempMediaList + Private Property IPluginContentProvider_TempMediaList As List(Of IUserMedia) Implements IPluginContentProvider.TempMediaList Private Property IPluginContentProvider_SeparateVideoFolder As Boolean Implements IPluginContentProvider.SeparateVideoFolder Private Property IPluginContentProvider_DataPath As String Implements IPluginContentProvider.DataPath Private Sub IPluginContentProvider_XmlFieldsSet(ByVal Fields As List(Of KeyValuePair(Of String, String))) Implements IPluginContentProvider.XmlFieldsSet @@ -620,7 +645,7 @@ BlockNullPicture: With DirectCast(u, UserDataBase) If Not .User.Plugin.IsEmptyString Then uName = .User.Name - Return Settings(.User.Plugin).GetUserPostUrl(.ID, PostData.Post.ID) + Return Settings(.User.Plugin).GetUserPostUrl(.Self, PostData) End If End With End If @@ -657,7 +682,13 @@ BlockNullPicture: LastUpdated = AConvert(Of Date)(x.Value(Name_LastUpdated), ADateTime.Formats.BaseDateTime, Nothing) ScriptUse = x.Value(Name_ScriptUse).FromXML(Of Boolean)(False) ScriptData = x.Value(Name_ScriptData) - DataMerging = x.Value(Name_DataMerging).FromXML(Of Boolean)(False) +#Disable Warning BC40000 + If x.Contains(Name_DataMerging) Then + DataMerging = x.Value(Name_DataMerging).FromXML(Of Boolean)(False) + Else + DataMerging = x.Value(Name_Merged).FromXML(Of Boolean)(False) + End If +#Enable Warning ChangeCollectionName(x.Value(Name_CollectionName), False) Labels.ListAddList(x.Value(Name_LabelsName).StringToList(Of String, List(Of String))("|", EDP.ReturnValue), LAP.NotContainsOnly, LAP.ClearBeforeAdd) LoadUserInformation_OptionalFields(x, True) @@ -675,7 +706,13 @@ BlockNullPicture: MyFile.Exists(SFO.Path) Using x As New XmlFile With {.Name = "User"} x.Add(Name_Site, Site) + x.Add(Name_Plugin, HOST.Key) x.Add(Name_UserName, User.Name) + x.Add(Name_IsChannel, IsChannel.BoolToInteger) + x.Add(Name_Model_User, CInt(UserModel)) + x.Add(Name_Model_Collection, CInt(CollectionModel)) + x.Add(Name_SpecialPath, User.SpecialPath) + x.Add(Name_SpecialCollectionPath, User.SpecialCollectionPath) x.Add(Name_UserExists, UserExists.BoolToInteger) x.Add(Name_UserSuspended, UserSuspended.BoolToInteger) x.Add(Name_UserID, ID) @@ -700,7 +737,7 @@ BlockNullPicture: x.Add(Name_ScriptData, ScriptData) x.Add(Name_CollectionName, CollectionName) x.Add(Name_LabelsName, Labels.ListToString("|", EDP.ReturnValue)) - x.Add(Name_DataMerging, DataMerging.BoolToInteger) + x.Add(Name_Merged, DataMerging.BoolToInteger) LoadUserInformation_OptionalFields(x, False) @@ -748,7 +785,7 @@ BlockNullPicture: #Region "Open site, folder" Friend Overridable Sub OpenSite(Optional ByVal e As ErrorsDescriber = Nothing) Implements IContentProvider.OpenSite Try - Dim URL$ = HOST.Source.GetUserUrl(Name, IsChannel) + Dim URL$ = HOST.Source.GetUserUrl(Me, IsChannel) If Not URL.IsEmptyString Then Process.Start(URL) Catch ex As Exception If Not e.Exists Then e = New ErrorsDescriber(EDP.ShowAllMsg) @@ -817,7 +854,7 @@ BlockNullPicture: If Not Responser Is Nothing Then Responser.Dispose() Responser = New Response If Not HOST.Responser Is Nothing Then Responser.Copy(HOST.Responser) - 'TODO: remove + 'TODO: UserDataBase remove [Responser.DecodersError] Responser.DecodersError = New ErrorsDescriber(EDP.SendInLog + EDP.ReturnValue) With { .DeclaredMessage = New MMessage($"SymbolsConverter error: [{ToStringForLog()}]", ToStringForLog())} @@ -831,6 +868,7 @@ BlockNullPicture: DownloadedVideos(False) = 0 _TempMediaList.Clear() _TempPostsList.Clear() + LatestData.Clear() Dim __SaveData As Boolean = Not CreatedByChannel Or Not Settings.FromChannelDownloadTopUse LoadContentInformation() @@ -862,7 +900,7 @@ BlockNullPicture: DownloadContent(Token) ThrowIfDisposed() - LatestData.ListAddList(_ContentNew.Where(_downContent), LNC) + If IncludeInTheFeed Then LatestData.ListAddList(_ContentNew.Where(_downContent), LNC) Dim mcb& = If(ContentMissingExists, _ContentList.LongCount(Function(c) MissingFinder(c)), 0) _ContentList.ListAddList(_ContentNew.Where(Function(c) _downContent(c) Or MissingFinder(c)), LNC) Dim mca& = If(ContentMissingExists, _ContentList.LongCount(Function(c) MissingFinder(c)), 0) @@ -887,7 +925,7 @@ BlockNullPicture: ThrowIfDisposed() If UpPic Or EnvirChanged.Invoke Then OnUserUpdated() Catch oex As OperationCanceledException When Token.IsCancellationRequested - MyMainLOG = $"{Site} - {Name}: downloading canceled" + MyMainLOG = $"{ToStringForLog()}: downloading canceled" Canceled = True Catch dex As ObjectDisposedException When Disposed Canceled = True @@ -926,11 +964,7 @@ BlockNullPicture: Protected Overridable Sub ReparseMissing(ByVal Token As CancellationToken) End Sub Protected MustOverride Sub DownloadContent(ByVal Token As CancellationToken) - Private NotInheritable Class OptionalWebClient : Implements IDisposable - Private ReadOnly WC As WebClient - Private ReadOnly RC As Response - Private ReadOnly RCERROR As New ErrorsDescriber(EDP.ThrowException) - Private ReadOnly UseResponserClient As Boolean + Private NotInheritable Class OptionalWebClient : Inherits DownloadObjects.WebClient2 Friend Sub New(ByRef Source As UserDataBase) UseResponserClient = Source.UseResponserClient If UseResponserClient Then @@ -939,28 +973,6 @@ BlockNullPicture: WC = New WebClient End If End Sub - Friend Sub DownloadFile(ByVal URL As String, ByVal File As String) - If UseResponserClient Then - RC.DownloadFile(URL, File, RCERROR) - Else - WC.DownloadFile(URL, File) - End If - End Sub -#Region "IDisposable Support" - Private disposedValue As Boolean = False - Protected Sub Dispose(ByVal disposing As Boolean) - If Not disposedValue And disposing And Not WC Is Nothing Then WC.Dispose() - disposedValue = True - End Sub - Protected Overrides Sub Finalize() - Dispose(False) - MyBase.Finalize() - End Sub - Friend Sub Dispose() Implements IDisposable.Dispose - Dispose(True) - GC.SuppressFinalize(Me) - End Sub -#End Region End Class Protected Sub DownloadContentDefault(ByVal Token As CancellationToken) Try @@ -1068,9 +1080,12 @@ BlockNullPicture: Protected Overridable Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile) As SFile Return Nothing End Function + Protected Const EXCEPTION_OPERATION_CANCELED As Integer = -1 ''' Request DownloadingException ''' 0 - exit - Protected Function ProcessException(ByVal ex As Exception, ByVal Token As CancellationToken, ByVal Message As String, Optional ByVal RDE As Boolean = True, Optional ByVal EObj As Object = Nothing) As Integer + Protected Function ProcessException(ByVal ex As Exception, ByVal Token As CancellationToken, ByVal Message As String, + Optional ByVal RDE As Boolean = True, Optional ByVal EObj As Object = Nothing, + Optional ByVal ThrowEx As Boolean = True) As Integer If Not ((TypeOf ex Is OperationCanceledException And Token.IsCancellationRequested) Or (TypeOf ex Is ObjectDisposedException And Disposed)) Then If RDE Then @@ -1078,6 +1093,9 @@ BlockNullPicture: If v = 0 Then LogError(ex, Message) : HasError = True Return v End If + Else + 'URGENT: UserDataBase.ProcessException [Throw ex] + If ThrowEx Then Throw ex Else Return EXCEPTION_OPERATION_CANCELED End If Return 0 End Function @@ -1124,7 +1142,7 @@ BlockNullPicture: End Sub #End Region #Region "Delete, Move, Merge, Copy" - Friend Overridable Function Delete(Optional ByVal Multiple As Boolean = False) As Integer Implements IUserData.Delete + 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 If Not IncludedInCollection Then MainFrameObj.ImageHandler(Me, False) @@ -1138,44 +1156,51 @@ BlockNullPicture: Return 0 End If End Function - Friend Overridable Function MoveFiles(ByVal __CollectionName As String) As Boolean Implements IUserData.MoveFiles + Friend Overridable Function MoveFiles(ByVal __CollectionName As String, ByVal __SpecialCollectionPath As SFile) As Boolean Implements IUserData.MoveFiles Dim UserBefore As UserInfo = User Dim Removed As Boolean = True Dim _TurnBack As Boolean = False Try Dim f As SFile + Dim v As Boolean = IsVirtual If IncludedInCollection Then Settings.Users.Add(Me) Removed = False User.CollectionName = String.Empty - User.IncludedInCollection = False + User.SpecialCollectionPath = String.Empty + User.UserModel = UsageModel.Default + User.CollectionModel = UsageModel.Default Else Settings.Users.Remove(Me) Removed = True User.CollectionName = __CollectionName - User.IncludedInCollection = True - User.SpecialPath = Nothing + User.SpecialCollectionPath = __SpecialCollectionPath + If Not IsVirtual Then User.SpecialPath = Nothing End If _TurnBack = True User.UpdateUserFile() - f = User.File.CutPath(, EDP.ThrowException) - If f.Exists(SFO.Path, False) Then - If If(SFile.GetFiles(f,, SearchOption.AllDirectories), New List(Of SFile)).Count > 0 AndAlso - MsgBoxE({$"Destination directory [{f.Path}] already exists and contains files!" & vbCr & - "By continuing, this directory and all files will be deleted", - "Destination directory is not empty!"}, MsgBoxStyle.Exclamation,,, {"Delete", "Cancel"}) = 1 Then - MsgBoxE("Operation canceled", MsgBoxStyle.Exclamation) - User = UserBefore - If Removed Then Settings.Users.Add(Me) Else Settings.Users.Remove(Me) - _TurnBack = False - Return False + + If Not v Then + f = User.File.CutPath(, EDP.ThrowException) + If f.Exists(SFO.Path, False) Then + If If(SFile.GetFiles(f,, SearchOption.AllDirectories), New List(Of SFile)).Count > 0 AndAlso + MsgBoxE({$"Destination directory [{f.Path}] already exists and contains files!" & vbCr & + "By continuing, this directory and all files will be deleted", + "Destination directory is not empty!"}, MsgBoxStyle.Exclamation,,, {"Delete", "Cancel"}) = 1 Then + MsgBoxE("Operation canceled", MsgBoxStyle.Exclamation) + User = UserBefore + If Removed Then Settings.Users.Add(Me) Else Settings.Users.Remove(Me) + _TurnBack = False + Return False + End If + f.Delete(SFO.Path, Settings.DeleteMode, EDP.ThrowException) End If - f.Delete(SFO.Path, Settings.DeleteMode, EDP.ThrowException) + f.CutPath.Exists(SFO.Path) + Directory.Move(UserBefore.File.CutPath(, EDP.ThrowException).Path, f.Path) + If Not ScriptData.IsEmptyString AndAlso ScriptData.Contains(UserBefore.File.PathNoSeparator) Then _ + ScriptData = ScriptData.Replace(UserBefore.File.PathNoSeparator, MyFile.PathNoSeparator) End If - f.CutPath.Exists(SFO.Path) - Directory.Move(UserBefore.File.CutPath(, EDP.ThrowException).Path, f.Path) - If Not ScriptData.IsEmptyString AndAlso ScriptData.Contains(UserBefore.File.PathNoSeparator) Then _ - ScriptData = ScriptData.Replace(UserBefore.File.PathNoSeparator, MyFile.PathNoSeparator) + Settings.UsersList.Remove(UserBefore) Settings.UpdateUsersList(User) UpdateUserInformation() @@ -1411,6 +1436,7 @@ BlockNullPicture: End Sub #End Region End Class +#Region "Base interfaces" Friend Interface IContentProvider ReadOnly Property Site As String Property Name As String @@ -1433,6 +1459,9 @@ BlockNullPicture: ReadOnly Property IsCollection As Boolean Property CollectionName As String ReadOnly Property IncludedInCollection As Boolean + ReadOnly Property UserModel As UsageModel + ReadOnly Property CollectionModel As UsageModel + ReadOnly Property IsVirtual As Boolean ReadOnly Property Labels As List(Of String) #End Region ReadOnly Property IsChannel As Boolean @@ -1464,8 +1493,8 @@ BlockNullPicture: ''' 2 - Collection removed
''' 3 - Collection split ''' - Function Delete(Optional ByVal Multiple As Boolean = False) As Integer - Function MoveFiles(ByVal CollectionName As String) As Boolean + Function Delete(Optional ByVal Multiple As Boolean = False, Optional ByVal CollectionValue As Integer = -1) As Integer + Function MoveFiles(ByVal CollectionName As String, ByVal SpecialCollectionPath As SFile) As Boolean Function CopyFiles(ByVal DestinationPath As SFile, Optional ByVal e As ErrorsDescriber = Nothing) As Boolean Sub OpenFolder() ReadOnly Property Self As IUserData @@ -1488,4 +1517,5 @@ BlockNullPicture: Property SkipExistsUsers As Boolean Property SaveToCache As Boolean End Interface +#End Region End Namespace \ No newline at end of file diff --git a/SCrawler/API/BaseObjects/DomainEnvir.vb b/SCrawler/API/BaseObjects/DomainEnvir.vb new file mode 100644 index 0000000..b9374eb --- /dev/null +++ b/SCrawler/API/BaseObjects/DomainEnvir.vb @@ -0,0 +1,86 @@ +' 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 PersonalUtilities.Forms +Imports ADB = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons +Namespace API.BaseObjects + Friend Interface IDomainContainer + ReadOnly Property Icon As Icon + ReadOnly Property Site As String + ReadOnly Property Domains As List(Of String) + ReadOnly Property DomainsTemp As List(Of String) + ReadOnly Property DomainsDefault As String + ReadOnly Property DomainsSettingProp As Plugin.PropertyValue + Property DomainsChanged As Boolean + Property Initialized As Boolean + Property DomainsUpdateInProgress As Boolean + Property DomainsUpdatedBySite As Boolean + Sub UpdateDomains() + End Interface + Friend NotInheritable Class DomainContainer + Private Sub New() + End Sub + Friend Shared Sub EndInit(ByVal s As IDomainContainer) + If ACheck(s.DomainsSettingProp.Value) Then s.Domains.ListAddList(CStr(s.DomainsSettingProp.Value).Split("|"), LAP.NotContainsOnly) + End Sub + Friend Overloads Shared Sub UpdateDomains(ByVal s As IDomainContainer) + UpdateDomains(s, Nothing, True) + End Sub + Friend Overloads Shared Sub UpdateDomains(ByVal s As IDomainContainer, ByVal NewDomains As IEnumerable(Of String), ByVal Internal As Boolean) + With s + If Not .Initialized Or (.DomainsUpdatedBySite And Not Internal) Then Exit Sub + If Not .DomainsUpdateInProgress Then + .DomainsUpdateInProgress = True + .Domains.ListAddList(.DomainsDefault.Split("|"), LAP.NotContainsOnly) + .Domains.ListAddList(NewDomains, LAP.NotContainsOnly) + .DomainsSettingProp.Value = .Domains.ListToString("|") + If Not Internal Then .DomainsUpdatedBySite = True + .DomainsUpdateInProgress = False + End If + End With + End Sub + Friend Shared Sub Update(ByVal s As IDomainContainer) + With s + If .DomainsChanged Then + .Domains.Clear() + .Domains.ListAddList(.DomainsTemp, LAP.NotContainsOnly) + .UpdateDomains() + End If + End With + End Sub + Friend Shared Sub EndEdit(ByVal s As IDomainContainer) + s.DomainsTemp.ListAddList(s.Domains, LAP.ClearBeforeAdd, LAP.NotContainsOnly) + s.DomainsChanged = False + End Sub + Friend Shared Sub OpenSettingsForm(ByVal s As IDomainContainer) + Dim __add As EventHandler(Of SimpleListFormEventArgs) = Sub(sender, e) e.ValueNew = InputBoxE($"Enter a new domain using the pattern [{s.Site}.com]:", "New domain").IfNullOrEmptyE(Nothing) + Dim __delete As EventHandler(Of SimpleListFormEventArgs) = Sub(sender, e) + Dim n$ = AConvert(Of String)(e.ValueCurrent, AModes.Var, String.Empty) + e.Result = MsgBoxE({$"Are you sure you want to delete the [{n}] domain?", + "Removing domains"}, vbYesNo) = vbYes + End Sub + Using f As New SimpleListForm(Of String)(If(s.DomainsChanged, s.DomainsTemp, s.Domains), Settings.Design) With { + .Buttons = {ADB.Add, ADB.Delete}, + .Mode = SimpleListFormModes.Remaining, + .FormText = s.Site, + .Icon = s.Icon, + .LocationOnly = True, + .Size = New Size(400, 330), + .DesignXMLNode = s.Site + } + AddHandler f.AddClick, __add + AddHandler f.DeleteClick, __delete + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then + s.DomainsChanged = True + s.DomainsTemp.ListAddList(f.DataResult, LAP.ClearBeforeAdd, LAP.NotContainsOnly) + End If + End Using + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Imgur/Envir.vb b/SCrawler/API/Imgur/Envir.vb index dedff32..fbb4b8b 100644 --- a/SCrawler/API/Imgur/Envir.vb +++ b/SCrawler/API/Imgur/Envir.vb @@ -11,7 +11,7 @@ Imports SCrawler.API.Base Imports SCrawler.API.Imgur.Declarations Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Documents.JSON Namespace API.Imgur Namespace Declarations Friend Module Imgur_Declarations diff --git a/SCrawler/API/Instagram/SiteSettings.vb b/SCrawler/API/Instagram/SiteSettings.vb index 6fe2ecf..b83d27f 100644 --- a/SCrawler/API/Instagram/SiteSettings.vb +++ b/SCrawler/API/Instagram/SiteSettings.vb @@ -89,7 +89,7 @@ Namespace API.Instagram Friend Property IG_WWW_CLAIM As PropertyValue Private Const SavedPostsUserName_Text As String = "Saved posts user" - + Friend ReadOnly Property SavedPostsUserName As PropertyValue Friend Overrides Function BaseAuthExists() As Boolean Return If(Responser.Cookies?.Count, 0) > 0 And ACheck(IG_APP_ID.Value) And ACheck(IG_WWW_CLAIM.Value) And ACheck(CSRF_TOKEN.Value) @@ -106,8 +106,8 @@ Namespace API.Instagram Case NameOf(CSRF_TOKEN) : f = Header_CSRF_TOKEN End Select If Not f.IsEmptyString Then - If Responser.Headers.Count > 0 AndAlso Responser.Headers.ContainsKey(f) Then Responser.Headers.Remove(f) - If Not CStr(Value).IsEmptyString Then Responser.Headers.Add(f, CStr(Value)) + Responser.HeadersRemove(f) + If Not CStr(Value).IsEmptyString Then Responser.HeadersAdd(f, CStr(Value)) Responser.SaveSettings() End If End If @@ -202,11 +202,9 @@ Namespace API.Instagram With Responser If .Headers.Count > 0 Then - With .Headers - If .ContainsKey(Header_CSRF_TOKEN) Then token = .Item(Header_CSRF_TOKEN) - If .ContainsKey(Header_IG_APP_ID) Then app_id = .Item(Header_IG_APP_ID) - If .ContainsKey(Header_IG_WWW_CLAIM) Then www_claim = .Item(Header_IG_WWW_CLAIM) - End With + token = .HeadersValue(Header_CSRF_TOKEN) + app_id = .HeadersValue(Header_IG_APP_ID) + www_claim = .HeadersValue(Header_IG_WWW_CLAIM) End If If Not .Cookies Is Nothing Then .Cookies.ChangedAllowInternalDrop = False @@ -263,10 +261,10 @@ Namespace API.Instagram With Source Hash = AConvert(Of String)(.Hash.Value, String.Empty) Hash2 = AConvert(Of String)(.HashSavedPosts.Value, String.Empty) - With .Responser.Headers - If .ContainsKey(Header_CSRF_TOKEN) Then Token = .Item(Header_CSRF_TOKEN) - If .ContainsKey(Header_IG_APP_ID) Then AppID = .Item(Header_IG_APP_ID) - If .ContainsKey(Header_IG_WWW_CLAIM) Then WwwClaim = .Item(Header_IG_WWW_CLAIM) + With .Responser + Token = .HeadersValue(Header_CSRF_TOKEN) + AppID = .HeadersValue(Header_IG_APP_ID) + WwwClaim = .HeadersValue(Header_IG_WWW_CLAIM) End With End With End Sub diff --git a/SCrawler/API/Instagram/UserData.vb b/SCrawler/API/Instagram/UserData.vb index 82c4ef2..bb12f00 100644 --- a/SCrawler/API/Instagram/UserData.vb +++ b/SCrawler/API/Instagram/UserData.vb @@ -11,8 +11,8 @@ Imports System.Threading Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.Messaging Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports SCrawler.API.Base Imports UTypes = SCrawler.API.Base.UserMedia.Types Namespace API.Instagram @@ -510,6 +510,7 @@ Namespace API.Instagram 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 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 Return elem.Value("taken_at") ElseIf elem.Contains("imported_taken_at") Then @@ -518,7 +519,7 @@ Namespace API.Instagram Dim ev$ = elem.Value("device_timestamp") If Not ev.IsEmptyString Then If ev.Length > 10 Then - Return elem.Value("device_timestamp").Substring(0, 10) + Return ev.Substring(0, 10) Else Return ev End If diff --git a/SCrawler/API/LPSG/UserData.vb b/SCrawler/API/LPSG/UserData.vb index 2c3fff6..2d9cad7 100644 --- a/SCrawler/API/LPSG/UserData.vb +++ b/SCrawler/API/LPSG/UserData.vb @@ -10,6 +10,7 @@ Imports System.Threading Imports SCrawler.API.Base Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Clients Imports UTypes = SCrawler.API.Base.UserMedia.Types Imports Converters = PersonalUtilities.Functions.SymbolsConverter.Converters Namespace API.LPSG @@ -27,7 +28,7 @@ Namespace API.LPSG Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) Dim URL$ = String.Empty Try - Responser.Error = EDP.ThrowException + Responser.DeclaredError = EDP.ThrowException Dim NextPage$ Dim r$ @@ -87,7 +88,7 @@ Namespace API.LPSG End If End Sub Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) - With Responser : .UseWebClient = True : .UseWebClientCookies = True : .ResetError() : End With + With Responser : .Mode = Response.Modes.WebClient : .ResetStatus() : End With UseResponserClient = True DownloadContentDefault(Token) End Sub diff --git a/SCrawler/API/PornHub/Declarations.vb b/SCrawler/API/PornHub/Declarations.vb new file mode 100644 index 0000000..cf5897b --- /dev/null +++ b/SCrawler/API/PornHub/Declarations.vb @@ -0,0 +1,44 @@ +' 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 PersonalUtilities.Functions.RegularExpressions +Namespace API.PornHub + Friend Module Declarations +#Region "Converters" + Private ReadOnly UnicodeHexConverter As Func(Of String, String) = Function(Input) SymbolsConverter.UnicodeHex.Decode(Input, EDP.ReturnValue) + Friend ReadOnly HtmlConverter As Func(Of String, String) = Function(Input) SymbolsConverter.HTML.Decode(Input, EDP.ReturnValue) +#End Region +#Region "Declarations video" + Friend ReadOnly RegexVideo_FlashVarsBlock As RParams = RParams.DM("(?<=flashvars_\['[nN]ext[vV]ideo'\];[\r\n]*?)(.+?)(?=;flashvars_\d+?)", 0, EDP.ReturnValue) + Friend ReadOnly RegexVideo_FlashVars_Vars As RParams = RParams.DM("var ([\w\d]{10,})=("".+?)(?=(;|\Z))", 0, RegexReturn.List) + Friend ReadOnly RegexVideo_FlashVars_Compiler As RParams = RParams.DM("(?<=\*/)([\w\d\S]{10,})", 0, RegexReturn.List) + Friend ReadOnly RegexVideo_Video_All As RParams = RParams.DM("div class=""thumbnail-info-wrapper clearfix.+?[\r\n\s]*?\", 0, RegexReturn.List, EDP.ReturnValue) + Friend ReadOnly Regex_Gif_UrlName As RParams = RParams.DMS("""name"":.*?""([^""]*)""[^\}]+?""contentUrl"":.*?""([^""]+)""", 0, RegexReturn.ListByMatch, EDP.ReturnValue) +#End Region +#Region "Declarations photo" + Friend ReadOnly Regex_Photo_ModelHub_PhotoBlocks As RParams = RParams.DM("var PHOTOS_ARRAY_(\d+) = \{[\r\n\s]*?(urls:.*?\[[^]]*\])", 0, RegexReturn.List, EDP.ReturnValue) + Friend ReadOnly Regex_Photo_PornHub_PhotoBlocks As RParams = RParams.DM("photoAlbumListContainer[\r\n\s\S]+?title=""([^""]+)""[\r\n\s\S]+?a href=""(/album/\d+)""", 0, RegexReturn.List) + Friend ReadOnly Regex_Photo_PornHub_AlbumPhotoArr As RParams = RParams.DMS("\ + Partial Friend Class OptionsForm : Inherits System.Windows.Forms.Form + + Protected Overrides Sub Dispose(ByVal disposing As Boolean) + Try + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + Finally + MyBase.Dispose(disposing) + End Try + End Sub + Private components As System.ComponentModel.IContainer + + Private Sub InitializeComponent() + Dim CONTAINER_MAIN As System.Windows.Forms.ToolStripContainer + Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel + Me.CH_DOWN_GIFS = New System.Windows.Forms.CheckBox() + Me.CH_DOWN_PHOTO_MODELHUB = New System.Windows.Forms.CheckBox() + CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + CONTAINER_MAIN.ContentPanel.SuspendLayout() + CONTAINER_MAIN.SuspendLayout() + TP_MAIN.SuspendLayout() + Me.SuspendLayout() + ' + 'CONTAINER_MAIN + ' + ' + 'CONTAINER_MAIN.ContentPanel + ' + CONTAINER_MAIN.ContentPanel.Controls.Add(TP_MAIN) + CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(278, 52) + CONTAINER_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + CONTAINER_MAIN.LeftToolStripPanelVisible = False + CONTAINER_MAIN.Location = New System.Drawing.Point(0, 0) + CONTAINER_MAIN.Name = "CONTAINER_MAIN" + CONTAINER_MAIN.RightToolStripPanelVisible = False + CONTAINER_MAIN.Size = New System.Drawing.Size(278, 77) + CONTAINER_MAIN.TabIndex = 0 + CONTAINER_MAIN.TopToolStripPanelVisible = False + ' + 'TP_MAIN + ' + TP_MAIN.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.[Single] + TP_MAIN.ColumnCount = 1 + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Controls.Add(Me.CH_DOWN_GIFS, 0, 0) + TP_MAIN.Controls.Add(Me.CH_DOWN_PHOTO_MODELHUB, 0, 1) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 3 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Size = New System.Drawing.Size(278, 52) + TP_MAIN.TabIndex = 0 + ' + 'CH_DOWN_GIFS + ' + Me.CH_DOWN_GIFS.AutoSize = True + Me.CH_DOWN_GIFS.Dock = System.Windows.Forms.DockStyle.Fill + Me.CH_DOWN_GIFS.Location = New System.Drawing.Point(4, 4) + Me.CH_DOWN_GIFS.Name = "CH_DOWN_GIFS" + Me.CH_DOWN_GIFS.Size = New System.Drawing.Size(270, 19) + Me.CH_DOWN_GIFS.TabIndex = 0 + Me.CH_DOWN_GIFS.Text = "Download gifs" + Me.CH_DOWN_GIFS.UseVisualStyleBackColor = True + ' + 'CH_DOWN_PHOTO_MODELHUB + ' + Me.CH_DOWN_PHOTO_MODELHUB.AutoSize = True + Me.CH_DOWN_PHOTO_MODELHUB.Dock = System.Windows.Forms.DockStyle.Fill + Me.CH_DOWN_PHOTO_MODELHUB.Location = New System.Drawing.Point(4, 30) + Me.CH_DOWN_PHOTO_MODELHUB.Name = "CH_DOWN_PHOTO_MODELHUB" + Me.CH_DOWN_PHOTO_MODELHUB.Size = New System.Drawing.Size(270, 19) + Me.CH_DOWN_PHOTO_MODELHUB.TabIndex = 1 + Me.CH_DOWN_PHOTO_MODELHUB.Text = "Download photo only from ModelHub" + Me.CH_DOWN_PHOTO_MODELHUB.UseVisualStyleBackColor = True + ' + 'OptionsForm + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(278, 77) + Me.Controls.Add(CONTAINER_MAIN) + Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle + Me.Icon = Global.SCrawler.My.Resources.SiteResources.InstagramIcon_32 + Me.KeyPreview = True + Me.MaximizeBox = False + Me.MaximumSize = New System.Drawing.Size(294, 116) + Me.MinimizeBox = False + Me.MinimumSize = New System.Drawing.Size(294, 116) + Me.Name = "OptionsForm" + Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide + Me.Text = "Options" + CONTAINER_MAIN.ContentPanel.ResumeLayout(False) + CONTAINER_MAIN.ResumeLayout(False) + CONTAINER_MAIN.PerformLayout() + TP_MAIN.ResumeLayout(False) + TP_MAIN.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Private WithEvents CH_DOWN_GIFS As CheckBox + Private WithEvents CH_DOWN_PHOTO_MODELHUB As CheckBox + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/XVIDEOS/SettingsForm.resx b/SCrawler/API/PornHub/OptionsForm.resx similarity index 97% rename from SCrawler/API/XVIDEOS/SettingsForm.resx rename to SCrawler/API/PornHub/OptionsForm.resx index 56d6ce4..be8e932 100644 --- a/SCrawler/API/XVIDEOS/SettingsForm.resx +++ b/SCrawler/API/PornHub/OptionsForm.resx @@ -120,4 +120,7 @@ False + + False + \ No newline at end of file diff --git a/SCrawler/API/PornHub/OptionsForm.vb b/SCrawler/API/PornHub/OptionsForm.vb new file mode 100644 index 0000000..69ca859 --- /dev/null +++ b/SCrawler/API/PornHub/OptionsForm.vb @@ -0,0 +1,34 @@ +' 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 PersonalUtilities.Forms +Namespace API.PornHub + Friend Class OptionsForm + Private WithEvents MyDefs As DefaultFormOptions + Private ReadOnly MyExchangeOptions As UserExchangeOptions + Friend Sub New(ByRef ExchangeOptions As UserExchangeOptions) + InitializeComponent() + MyExchangeOptions = ExchangeOptions + MyDefs = New DefaultFormOptions(Me, Settings.Design) + End Sub + Private Sub MyForm_Load(sender As Object, e As EventArgs) Handles Me.Load + With MyDefs + .MyViewInitialize(True) + .AddOkCancelToolbar() + CH_DOWN_GIFS.Checked = MyExchangeOptions.DownloadGifs + CH_DOWN_PHOTO_MODELHUB.Checked = MyExchangeOptions.DownloadPhotoOnlyFromModelHub + .EndLoaderOperations() + End With + End Sub + Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick + MyExchangeOptions.DownloadGifs = CH_DOWN_GIFS.Checked + MyExchangeOptions.DownloadPhotoOnlyFromModelHub = CH_DOWN_PHOTO_MODELHUB.Checked + MyDefs.CloseForm() + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/SiteSettings.vb b/SCrawler/API/PornHub/SiteSettings.vb new file mode 100644 index 0000000..37afcca --- /dev/null +++ b/SCrawler/API/PornHub/SiteSettings.vb @@ -0,0 +1,123 @@ +' 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.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Clients +Namespace API.PornHub + + Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon + Get + Return My.Resources.SiteResources.PornHubIcon_16 + End Get + End Property + Friend Overrides ReadOnly Property Image As Image + Get + Return My.Resources.SiteResources.PornHubPic_16 + End Get + End Property + Private ReadOnly Property CurlPathExists As Boolean + + Friend ReadOnly Property DownloadGifs As PropertyValue + + Friend ReadOnly Property DownloadGifsAsMp4 As PropertyValue + + Friend ReadOnly Property DownloadPhotoOnlyFromModelHub As PropertyValue + + Friend ReadOnly Property SavedPostsUserName As PropertyValue +#End Region +#Region "Initializer" + Friend Sub New() + MyBase.New("PornHub", "pornhub.com") + Responser.CurlPath = $"cURL\curl.exe" + CurlPathExists = Responser.CurlPath.Exists + Responser.DeclaredError = EDP.ThrowException + + DownloadGifsAsMp4 = New PropertyValue(True) + DownloadGifs = New PropertyValue(CInt(CheckState.Indeterminate), GetType(Integer)) + DownloadPhotoOnlyFromModelHub = New PropertyValue(True) + SavedPostsUserName = New PropertyValue(String.Empty, GetType(String)) + + UrlPatternUser = "https://www.pornhub.com/{0}/{1}" + UserRegex = RParams.DMS("pornhub.com/([^/]+)/([^/]+).*?", 0, RegexReturn.ListByMatch) + ImageVideoContains = "pornhub" + End Sub +#End Region +#Region "GetInstance, GetSpecialData" + Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider + If What = ISiteSettings.Download.SavedPosts Then + Return New UserData With { + .IsSavedPosts = True, + .VideoPageModel = UserData.VideoPageModels.Favorite, + .PersonType = UserData.PersonTypeUser, + .User = New UserInfo With {.Name = $"{UserData.PersonTypeUser}_{CStr(AConvert(Of String)(SavedPostsUserName.Value, String.Empty))}"} + } + Else + Return New UserData + End If + End Function + Friend Overrides Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable + If Available(ISiteSettings.Download.Main, True) Then + Using resp As Response = Responser.Copy + Dim spf$ = String.Empty + Dim f As SFile = GetSpecialDataFile(Path, AskForPath, spf) + Dim m As UserMedia = UserData.GetVideoInfo(URL, resp, f) + If m.State = UserMedia.States.Downloaded Then + m.SpecialFolder = f + Return {m} + End If + End Using + End If + Return Nothing + End Function +#End Region +#Region "Downloading" + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + Return Settings.UseM3U8 And CurlPathExists And (Not What = ISiteSettings.Download.SavedPosts OrElse ACheck(SavedPostsUserName.Value)) + End Function +#End Region +#Region "IsMyUser" + Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions + Try + If Not UserURL.IsEmptyString Then + Dim alist As List(Of String) = RegexReplace(UserURL.ToLower, UserRegex) + If alist.ListExists(3) Then Return New ExchangeOptions(Site, $"{alist(1)}_{alist(2)}") + End If + Return Nothing + Catch ex As Exception + Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[API.PornHub.SiteSettings.IsMyUser({UserURL})]", New ExchangeOptions) + End Try + End Function +#End Region +#Region "GetUserUrl, GetUserPostUrl" + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String + With DirectCast(User, UserData) : Return String.Format(UrlPatternUser, .PersonType, .NameTrue) : End With + End Function + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + 'TODELETE: remove comment + Return Media.URL_BASE '$"https://www.pornhub.com/view_video.php?viewkey={Media.Post.ID}" + End Function +#End Region +#Region "User options" + Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) + Dim e As UserExchangeOptions = Nothing + If Not Options Is Nothing AndAlso TypeOf Options Is UserExchangeOptions Then e = Options + If e Is Nothing Then e = New UserExchangeOptions(Me) + If OpenForm Then + Using f As New OptionsForm(e) : f.ShowDialog() : End Using + End If + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/UserData.vb b/SCrawler/API/PornHub/UserData.vb new file mode 100644 index 0000000..993fbdb --- /dev/null +++ b/SCrawler/API/PornHub/UserData.vb @@ -0,0 +1,656 @@ +' 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 PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON +Imports UTypes = SCrawler.API.Base.UserMedia.Types +Namespace API.PornHub + Friend Class UserData : Inherits UserDataBase + Private Const UrlPattern As String = "https://www.pornhub.com/{0}" +#Region "Declarations" +#Region "XML names" + Private Const Name_PersonType As String = "PersonType" + Private Const Name_NameTrue As String = "NameTrue" + Private Const Name_VideoPageModel As String = "VideoPageModel" + Private Const Name_PhotoPageModel As String = "PhotoPageModel" + Private Const Name_DownloadGifs As String = "DownloadGifs" + Private Const Name_DownloadPhotoOnlyFromModelHub As String = "DownloadPhotoOnlyFromModelHub" +#End Region +#Region "Structures" + Private Structure FlashVar : Implements IRegExCreator + Friend Name As String + Friend Value As String + Public Shared Widening Operator CType(ByVal Name As String) As FlashVar + Return New FlashVar With {.Name = Name} + End Operator + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists(2) Then + Name = ParamsArray(0) + Value = ParamsArray(1) + If Not Value.IsEmptyString Then Value = Value.Replace(""" + """, String.Empty).Replace("""", String.Empty).StringTrim + End If + Return Me + End Function + Public Overrides Function Equals(ByVal Obj As Object) As Boolean + Return CType(Obj, FlashVar).Name = Name + End Function + End Structure + Private Structure UserVideo : Implements IRegExCreator + Friend URL As String + Friend ID As String + Friend Title As String + Friend Function ToUserMedia() As UserMedia + Return New UserMedia(URL, UTypes.VideoPre) With { + .File = If(Title.IsEmptyString, .File, New SFile($"{Title}.mp4")), + .Post = ID + } + End Function + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists Then + URL = ParamsArray(0) + ID = RegexReplace(URL, RegexVideo_Video_VideoKey) + URL = String.Format(UrlPattern, URL.TrimStart("/")) + Title = HtmlConverter(ParamsArray(1)).StringRemoveWinForbiddenSymbols.StringTrim + End If + Return Me + End Function + Public Overrides Function Equals(ByVal Obj As Object) As Boolean + Return DirectCast(Obj, UserVideo).URL = URL + End Function + End Structure + Private Structure PhotoBlock : Implements IRegExCreator + Friend AlbumID As String + Friend Data As String + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists(2) Then + AlbumID = ParamsArray(0) + Data = ParamsArray(1).StringTrim + End If + Return Me + End Function + End Structure +#End Region +#Region "Enums" + Friend Enum VideoPageModels As Integer + [Default] = 0 + ConcatPage = 1 + Favorite = 2 + Undefined = -1 + End Enum + Private Enum PhotoPageModels As Integer + Undefined = 0 + PornHubPage = 1 + ModelHubPage = 2 + End Enum +#End Region +#Region "Constants" + Private Const PersonTypeModel As String = "model" + Friend Const PersonTypeUser As String = "users" +#End Region +#Region "Person" + Friend Property PersonType As String + Friend Property NameTrue As String + Private _FriendlyName As String = String.Empty + Friend Overrides Property FriendlyName As String + Get + If _FriendlyName.IsEmptyString Then Return NameTrue Else Return _FriendlyName + End Get + Set(ByVal n As String) + _FriendlyName = n + End Set + End Property +#End Region +#Region "Advanced fields" + Friend Property VideoPageModel As VideoPageModels = VideoPageModels.Undefined + Private Property PhotoPageModel As PhotoPageModels = PhotoPageModels.Undefined + Friend Property DownloadGifs As Boolean + Friend Property DownloadPhotoOnlyFromModelHub As Boolean = True +#End Region +#Region "ExchangeOptions" + Friend Overrides Function ExchangeOptionsGet() As Object + Return New UserExchangeOptions(Me) + End Function + Friend Overrides Sub ExchangeOptionsSet(ByVal Obj As Object) + If Not Obj Is Nothing AndAlso TypeOf Obj Is UserExchangeOptions Then + With DirectCast(Obj, UserExchangeOptions) + DownloadGifs = .DownloadGifs + DownloadPhotoOnlyFromModelHub = .DownloadPhotoOnlyFromModelHub + End With + End If + End Sub +#End Region + Private ReadOnly Property MySettings As SiteSettings + Get + Return DirectCast(HOST.Source, SiteSettings) + End Get + End Property +#End Region +#Region "Initializer, loader" + Friend Sub New() + UseInternalM3U8Function = True + End Sub + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + With Container + Dim SetNames As Action = Sub() + If Not Name.IsEmptyString And NameTrue.IsEmptyString Then + Dim n$() = Name.Split("_") + If n.ListExists(2) Then + NameTrue = Name.Replace($"{n(0)}_", String.Empty) + PersonType = n(0) + If (PersonType = PersonTypeModel Or PersonType = PersonTypeUser) And + VideoPageModel = VideoPageModels.Undefined Then VideoPageModel = VideoPageModels.Default + End If + End If + End Sub + If Loading Then + PersonType = .Value(Name_PersonType) + NameTrue = .Value(Name_NameTrue) + VideoPageModel = .Value(Name_VideoPageModel).FromXML(Of Integer)(VideoPageModels.Undefined) + PhotoPageModel = .Value(Name_PhotoPageModel).FromXML(Of Integer)(PhotoPageModels.Undefined) + DownloadGifs = .Value(Name_DownloadGifs).FromXML(Of Integer)(False) + DownloadPhotoOnlyFromModelHub = .Value(Name_DownloadPhotoOnlyFromModelHub).FromXML(Of Boolean)(True) + SetNames.Invoke() + Else + SetNames.Invoke() + .Add(Name_PersonType, PersonType) + .Add(Name_NameTrue, NameTrue) + .Add(Name_VideoPageModel, CInt(VideoPageModel)) + .Add(Name_PhotoPageModel, CInt(PhotoPageModel)) + .Add(Name_DownloadGifs, DownloadGifs.BoolToInteger) + .Add(Name_DownloadPhotoOnlyFromModelHub, DownloadPhotoOnlyFromModelHub.BoolToInteger) + End If + End With + End Sub +#End Region +#Region "Downloading" +#Region "Download override" + Private Const DataDownloaded As Integer = -10 + Private Const DataDownloaded_NotFound As Integer = -20 + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + Try + Responser.ResetStatus() + If PersonType = PersonTypeUser Then Responser.Mode = Response.Modes.Curl + + If IsSavedPosts Then VideoPageModel = VideoPageModels.Favorite + + Dim page% = 1 + Dim __continue As Boolean = True + Dim __videoDone As Boolean = False + Dim d% + If DownloadVideos Then + If PersonType = PersonTypeUser Then Responser.Mode = Response.Modes.Curl : Responser.Method = "POST" + If VideoPageModel = VideoPageModels.Undefined Then + __continue = False + d = DownloadUserVideos(page, Token) + Select Case d + Case DataDownloaded : __continue = True : page += 1 + Case 1 : VideoPageModel = VideoPageModels.ConcatPage + Case EXCEPTION_OPERATION_CANCELED : ThrowAny(Token) + Case DataDownloaded_NotFound : __videoDone = True + End Select + If Not __continue And Not __videoDone Then + d = DownloadUserVideos(page, Token) + Select Case d + Case DataDownloaded : __continue = True : page += 1 + Case 1 : VideoPageModel = VideoPageModels.Undefined + Case EXCEPTION_OPERATION_CANCELED : ThrowAny(Token) + Case DataDownloaded_NotFound : __videoDone = True + End Select + End If + End If + If __continue And Not __videoDone Then + Do While DownloadUserVideos(page, Token) = DataDownloaded And page < 100 : page += 1 : Loop + End If + End If + + Responser.Method = "GET" + If DownloadGifs And Not IsSavedPosts Then DownloadUserGifs(Token) + If DownloadImages Then DownloadUserPhotos(Token) + Finally + Responser.Mode = Response.Modes.Default + Responser.Method = "GET" + End Try + End Sub +#End Region +#Region "Download video" + Private ReadOnly Property VideoPageType As String + Get + Select Case VideoPageModel + Case VideoPageModels.Default : Return "/videos/upload" + Case VideoPageModels.Favorite : Return "/videos/favorites/" + Case Else : Return String.Empty + End Select + End Get + End Property + Private ReadOnly Property VideoPageAppender As String + Get + Return If(PersonType = PersonTypeUser, "ajax?o=newest&page=", String.Empty) + End Get + End Property + Private Overloads Function DownloadUserVideos(ByVal Page As Integer, ByVal Token As CancellationToken) As Integer + Const VideoUrlPattern$ = "https://www.pornhub.com/{0}/{1}{2}{3}" + Const HtmlPageNotFoundVideo$ = "Error Page Not Found" + Dim URL$ = String.Empty + Try + Dim p$ + If PersonType = PersonTypeUser Then + p = Page + Else + p = IIf(Page = 1, String.Empty, $"?page={Page}") + End If + + URL = $"{String.Format(VideoUrlPattern, PersonType, NameTrue, VideoPageType, VideoPageAppender)}{p}" + ThrowAny(Token) + + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + If PersonType = PersonTypeUser And r.Contains(HtmlPageNotFoundVideo) Then Return DataDownloaded_NotFound + Dim l As List(Of UserVideo) = RegexFields(Of UserVideo)(r, {RegexVideo_Video_All}, {1, 2}) + Dim lw As List(Of UserVideo) = Nothing + If Not PersonType = PersonTypeUser Then RegexFields(Of UserVideo)(r, {RegexVideo_Video_Wrong}, RegexVideo_Video_Wrong_Fields) + If l.ListExists Then + If lw.ListExists Then l.ListWithRemove(lw) + If l.Count > 0 Then + Dim lBefore% = l.Count + l.RemoveAll(Function(ByVal uv As UserVideo) As Boolean + If Not _TempPostsList.Contains(uv.ID) Then + _TempPostsList.Add(uv.ID) + Return False + Else + Return True + End If + End Function) + If l.Count > 0 Then _TempMediaList.ListAddList(l.Select(Function(uv) uv.ToUserMedia)) + If l.Count = lBefore And l.Count > 0 Then Return DataDownloaded + End If + End If + End If + Return DataDownloaded_NotFound + Catch regex_ex As RegexFieldsTextBecameNullException + If PersonType = PersonTypeUser Or IsSavedPosts Then + Return DataDownloaded_NotFound + Else + Return ProcessException(regex_ex, Token, $"videos downloading error [{URL}]") + End If + Catch ex As Exception + Return ProcessException(ex, Token, $"videos downloading error [{URL}]") + End Try + End Function +#End Region +#Region "Download GIF" + Private Sub DownloadUserGifs(ByVal Token As CancellationToken) + Dim URL$ = $"https://www.pornhub.com/{PersonType}/{NameTrue}/gifs" + Try + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + Dim n$ + Dim m As UserMedia = Nothing + Dim l As List(Of RegexMatchStruct) = RegexFields(Of RegexMatchStruct)(r, {Regex_Gif_Array}, {1}) + Dim l2 As List(Of String) = Nothing + Dim l3 As List(Of String) = Nothing + If l.ListExists Then l2 = l.Select(Function(ll) $"gif/{ll.Arr(0).Replace("gif", String.Empty)}").ToList + If l2.ListExists Then + For Each gif$ In l2 + If Not _TempPostsList.Contains(gif) Then + _TempPostsList.Add(gif) + URL = $"https://www.pornhub.com/{gif}" + m = New UserMedia(URL, UTypes.Video) With {.Post = gif, .SpecialFolder = "GIFs\"} + ThrowAny(Token) + Try + r = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + If l3.ListExists Then l3.Clear() : l3 = Nothing + l3 = RegexReplace(r, Regex_Gif_UrlName) + If l3.ListExists(3) Then + m.URL = l3(2) + m.File = m.URL + n = HtmlConverter(l3(1)).StringRemoveWinForbiddenSymbols.StringTrim + If MySettings.DownloadGifsAsMp4.Value Then m.File.Extension = "mp4" + If Not n.IsEmptyString Then m.File.Name = n + End If + End If + Catch gif_down_ex As Exception + m.State = UserMedia.States.Missing + End Try + _TempMediaList.ListAddValue(m) + End If + Next + End If + If l.ListExists Then l.Clear() + If l2.ListExists Then l2.Clear() + If l3.ListExists Then l3.Clear() + End If + Catch ex As Exception + ProcessException(ex, Token, $"gifs downloading error [{URL}]") + End Try + End Sub +#End Region +#Region "Download photo" + Private Const PhotoUrlPattern_ModelHub As String = "https://www.modelhub.com/{0}/photos" + Private Const PhotoUrlPattern_PornHub As String = "https://www.pornhub.com/{0}/{1}/photos" + Private Sub DownloadUserPhotos(ByVal Token As CancellationToken) + Try + If IsSavedPosts Then + DownloadUserPhotos_SavedPosts(Token) + ElseIf PersonType = PersonTypeModel Then + If PhotoPageModel = PhotoPageModels.Undefined Then + If DownloadUserPhotos_ModelHub(Token) Then PhotoPageModel = PhotoPageModels.ModelHubPage + ThrowAny(Token) + If PhotoPageModel = PhotoPageModels.Undefined AndAlso DownloadPhotoOnlyFromModelHub AndAlso + DownloadUserPhotos_PornHub(Token) Then PhotoPageModel = PhotoPageModels.PornHubPage + Else + Select Case PhotoPageModel + Case PhotoPageModels.ModelHubPage : DownloadUserPhotos_ModelHub(Token) + Case PhotoPageModels.PornHubPage : If DownloadPhotoOnlyFromModelHub Then DownloadUserPhotos_PornHub(Token) + End Select + End If + ElseIf Not DownloadPhotoOnlyFromModelHub Then + DownloadUserPhotos_PornHub(Token) + End If + ThrowAny(Token) + Catch ex As Exception + ProcessException(ex, Token, $"photos downloading error") + End Try + End Sub + Private Function DownloadUserPhotos_ModelHub(ByVal Token As CancellationToken) As Boolean + Dim URL$ = String.Empty + Try + Dim jErr As New ErrorsDescriber(EDP.SendInLog + EDP.ReturnValue) + Dim albumName$ + If PersonType = PersonTypeModel Then + URL = String.Format(PhotoUrlPattern_ModelHub, NameTrue) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + Dim l As List(Of PhotoBlock) = RegexFields(Of PhotoBlock)(r, {Regex_Photo_ModelHub_PhotoBlocks}, {1, 2}) + If l.ListExists Then l.RemoveAll(Function(ll) ll.Data.IsEmptyString) + If l.ListExists Then + Dim albumRegex As RParams = RParams.DMS("", 1, EDP.ReturnValue) + For Each block As PhotoBlock In l + If Not _TempPostsList.Contains(block.AlbumID) Then _TempPostsList.Add(block.AlbumID) Else Continue For + albumRegex.Pattern = "
  • [\r\n\s]*?
    [\r\n\s]*?\<[^\>]*?alt=""([^""]*)""" + albumName = StringTrim(RegexReplace(r, albumRegex)) + If albumName.IsEmptyString Then albumName = block.AlbumID + Using j As EContainer = JsonDocument.Parse("{" & block.Data & "}", jErr) + If Not j Is Nothing Then + If If(j("urls")?.Count, 0) > 0 Then + _TempMediaList.ListAddList(j("urls").Select(Function(jj) _ + New UserMedia(jj.ItemF({0}).XmlIfNothingValue, UTypes.Picture) With { + .SpecialFolder = $"Albums\{albumName}\"}), LNC) + End If + End If + End Using + Next + l.Clear() + End If + End If + End If + Return True + Catch ex As Exception + ThrowAny(Token) + Return False + End Try + End Function + Private Overloads Function DownloadUserPhotos_PornHub(ByVal Token As CancellationToken) As Boolean + Try + Dim albumName$ + Dim page% + Dim r$ = Responser.GetResponse(String.Format(PhotoUrlPattern_PornHub, PersonType, NameTrue)) + If Not r.IsEmptyString Then + Dim l As List(Of PhotoBlock) = RegexFields(Of PhotoBlock)(r, {Regex_Photo_PornHub_PhotoBlocks}, {2, 1}) + If l.ListExists Then l.RemoveAll(Function(ll) ll.AlbumID.IsEmptyString) + If l.ListExists Then + For Each block As PhotoBlock In l + If Not _TempPostsList.Contains(block.AlbumID) Then _TempPostsList.Add(block.AlbumID) Else Continue For + albumName = block.Data + If albumName.IsEmptyString Then + albumName = block.AlbumID.Split("/").LastOrDefault.StringTrim + Else + albumName = HtmlConverter(albumName).StringRemoveWinForbiddenSymbols.StringTrim + End If + page = 1 + Do While DownloadUserPhotos_PornHub(page, block.AlbumID, albumName, Token) : page += 1 : Loop + Next + l.Clear() + End If + End If + Return True + Catch ex As Exception + ThrowAny(Token) + Return False + End Try + End Function + Private Overloads Function DownloadUserPhotos_PornHub(ByVal Page As Integer, ByVal AlbumID As String, ByVal AlbumName As String, + ByVal Token As CancellationToken) As Boolean + Try + Dim r$ = Responser.GetResponse($"https://www.pornhub.com{AlbumID}{IIf(Page = 1, String.Empty, $"?page={Page}")}") + If Not r.IsEmptyString Then + Dim l As List(Of String) = RegexReplace(r, Regex_Photo_PornHub_AlbumPhotoArr) + If l.ListExists Then l.RemoveAll(Function(_url) _url.IsEmptyString) + If l.ListExists Then + For Each url$ In l + ThrowAny(Token) + Try + r = Responser.GetResponse(url) + If Not r.IsEmptyString Then + url = RegexReplace(r, Regex_Photo_PornHub_SinglePhoto) + If Not url.IsEmptyString Then _ + _TempMediaList.ListAddValue(New UserMedia(url, UTypes.Picture) With {.SpecialFolder = $"Albums\{AlbumName}\"}, LNC) + End If + Catch + End Try + Next + l.Clear() + Return True + End If + End If + Return False + Catch ex As Exception + ThrowAny(Token) + Return False + End Try + End Function + Private Function DownloadUserPhotos_SavedPosts(ByVal Token As CancellationToken) As Boolean + Const HtmlPageNotFoundPhoto$ = "Page Not Found" + Dim URL$ = $"https://www.pornhub.com/{PersonType}/{NameTrue}/photos/favorites" + Try + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + If r.Contains(HtmlPageNotFoundPhoto) Then Return False + Dim urls As List(Of String) = RegexReplace(r, Regex_Photo_PornHub_AlbumPhotoArr) + If urls.ListExists Then + Dim NewUrl$ + Dim m As UserMedia + Dim l2 As List(Of UserMedia) = urls.Select(Function(__url) New UserMedia(__url, UTypes.Picture) With { + .Post = __url.Split("/").LastOrDefault}).ToList + urls.Clear() + If l2.ListExists Then l2.RemoveAll(Function(media) media.URL.IsEmptyString) + If l2.ListExists Then + Dim lBefore% = l2.Count + If _TempPostsList.Count > 0 Then l2.RemoveAll(Function(media) _TempPostsList.Contains(media.Post.ID)) + If l2.Count > 0 Then + For i% = 0 To l2.Count - 1 + m = l2(i) + ThrowAny(Token) + Try + r = Responser.GetResponse(m.URL) + If Not r.IsEmptyString Then + NewUrl = RegexReplace(r, Regex_Photo_PornHub_SinglePhoto) + If Not NewUrl.IsEmptyString Then + m.URL = NewUrl + m.File = NewUrl + _TempPostsList.ListAddValue(m.Post.ID, LNC) + Else + Throw New Exception + End If + End If + Catch + m.State = UserMedia.States.Missing + End Try + _TempMediaList.ListAddValue(m, LNC) + Next + End If + Return l2.Count = lBefore + End If + End If + End If + Return False + Catch ex As Exception + Return ProcessException(ex, Token, $"photos downloading error [{URL}]") + End Try + End Function +#End Region +#End Region +#Region "ReparseVideo" + Protected Overrides Sub ReparseVideo(ByVal Token As CancellationToken) + Const ERR_NEW_URL$ = "ERR_NEW_URL" + Dim URL$ = String.Empty + Try + If _TempMediaList.Count > 0 AndAlso _TempMediaList.Exists(Function(tm) tm.Type = UTypes.VideoPre) Then + Dim m As UserMedia + Dim r$, NewUrl$ + For i% = _TempMediaList.Count - 1 To 0 Step -1 + If _TempMediaList(i).Type = UTypes.VideoPre Then + m = _TempMediaList(i) + ThrowAny(Token) + Try + URL = m.URL + r = Responser.Curl(URL) + If Not r.IsEmptyString Then + NewUrl = CreateVideoURL(r) + If NewUrl.IsEmptyString Then + Throw New Exception With {.HelpLink = ERR_NEW_URL} + Else + m.URL = NewUrl + m.Type = UTypes.m3u8 + _TempMediaList(i) = m + End If + Else + _TempMediaList.RemoveAt(i) + End If + Catch mid_ex As Exception + If mid_ex.HelpLink = ERR_NEW_URL OrElse DownloadingException(mid_ex, "") = 1 Then + m.State = UserMedia.States.Missing + _TempMediaList(i) = m + Else + _TempMediaList.RemoveAt(i) + End If + End Try + End If + Next + End If + Catch ex As Exception + ProcessException(ex, Token, "video reparsing error", False) + End Try + End Sub +#End Region +#Region "ReparseMissing" + Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken) + Dim rList As New List(Of Integer) + Try + If ContentMissingExists Then + Dim m As UserMedia + Dim r$ + Dim eCurl As New ErrorsDescriber(EDP.ReturnValue) + For i% = 0 To _ContentList.Count - 1 + m = _ContentList(i) + If m.State = UserMedia.States.Missing AndAlso Not m.URL_BASE.IsEmptyString Then + ThrowAny(Token) + r = Responser.Curl(m.URL_BASE, eCurl) + If Not r.IsEmptyString Then + Dim NewUrl$ = CreateVideoURL(r) + If Not NewUrl.IsEmptyString Then + m.URL = NewUrl + _TempMediaList.ListAddValue(m, LNC) + rList.Add(i) + End If + End If + End If + Next + End If + Catch ex As Exception + ProcessException(ex, Token, "missing data downloading error") + Finally + 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 "Download content" + Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) + DownloadContentDefault(Token) + End Sub + Protected Overrides Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile) As SFile + Return M3U8.Download(URL, Responser, DestinationFile) + End Function +#End Region +#Region "CreateVideoURL" + Private Shared Function CreateVideoURL(ByVal r As String) As String + Try + Dim OutStr$ = String.Empty + If Not r.IsEmptyString Then + Dim _VarBlock$ = RegexReplace(r, RegexVideo_FlashVarsBlock) + If Not _VarBlock.IsEmptyString Then + Dim vars As List(Of FlashVar) = RegexFields(Of FlashVar)(_VarBlock, {RegexVideo_FlashVars_Vars}, {1, 2}) + Dim compiler As List(Of String) = RegexReplace(_VarBlock, RegexVideo_FlashVars_Compiler) + If vars.ListExists And compiler.ListExists Then + Dim v$ + Dim i% + For Each var$ In compiler + i = vars.IndexOf(var) + If i >= 0 Then + v = vars(i).Value + If Not v.IsEmptyString Then OutStr &= v + End If + Next + End If + End If + End If + Return OutStr + Catch ex As Exception + Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "[API.PornHub.UserData.CreateVideoURL]", String.Empty) + End Try + End Function +#End Region +#Region "Standalone downloader" + Friend Shared Function GetVideoInfo(ByVal URL As String, ByVal Responser As Response, ByVal Destination As SFile) As UserMedia + Try + Dim r$ = Responser.Curl(URL) + If Not r.IsEmptyString Then + Dim NewUrl$ = CreateVideoURL(r) + If Not NewUrl.IsEmptyString Then + Dim f As SFile = M3U8.Download(NewUrl, Responser, Destination) + If Not f.IsEmptyString Then Return New UserMedia With {.State = UserMedia.States.Downloaded} + End If + End If + Return Nothing + Catch ex As Exception + Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"PornHub standalone download error: [{URL}]", New UserMedia) + End Try + End Function +#End Region +#Region "Exceptions" + 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 + If Responser.Status = Net.WebExceptionStatus.ConnectionClosed Then + Return 1 + ElseIf Responser.StatusCode = Net.HttpStatusCode.ServiceUnavailable Then + Return 2 + Else + Return 0 + End If + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/UserExchangeOptions.vb b/SCrawler/API/PornHub/UserExchangeOptions.vb new file mode 100644 index 0000000..9ccc4b4 --- /dev/null +++ b/SCrawler/API/PornHub/UserExchangeOptions.vb @@ -0,0 +1,23 @@ +' 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 +Namespace API.PornHub + Friend Class UserExchangeOptions + Friend Property DownloadGifs As Boolean + Friend Property DownloadPhotoOnlyFromModelHub As Boolean + Friend Sub New(ByVal u As UserData) + DownloadGifs = u.DownloadGifs + DownloadPhotoOnlyFromModelHub = u.DownloadPhotoOnlyFromModelHub + End Sub + Friend Sub New(ByVal s As SiteSettings) + Dim v As CheckState = CInt(s.DownloadGifs.Value) + DownloadGifs = Not v = CheckState.Unchecked + DownloadPhotoOnlyFromModelHub = s.DownloadPhotoOnlyFromModelHub.Value + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Reddit/M3U8.vb b/SCrawler/API/Reddit/M3U8.vb index 4307d1a..7578d65 100644 --- a/SCrawler/API/Reddit/M3U8.vb +++ b/SCrawler/API/Reddit/M3U8.vb @@ -8,7 +8,7 @@ ' but WITHOUT ANY WARRANTY Imports System.Net Imports SCrawler.API.Reddit.M3U8_Declarations -Imports PersonalUtilities.Tools.WEB +Imports PersonalUtilities.Tools.Web Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Reddit Namespace M3U8_Declarations diff --git a/SCrawler/API/Reddit/SiteSettings.vb b/SCrawler/API/Reddit/SiteSettings.vb index a2bea8c..e16387c 100644 --- a/SCrawler/API/Reddit/SiteSettings.vb +++ b/SCrawler/API/Reddit/SiteSettings.vb @@ -25,7 +25,7 @@ Namespace API.Reddit Return My.Resources.SiteResources.RedditPic_512 End Get End Property - + Friend ReadOnly Property SavedPostsUserName As PropertyValue Friend ReadOnly Property UseM3U8 As PropertyValue @@ -40,6 +40,7 @@ Namespace API.Reddit UrlPatternUser = "https://www.reddit.com/user/{0}/" UrlPatternChannel = "https://www.reddit.com/r/{0}/" ImageVideoContains = "reddit.com" + UserRegex = RParams.DM("[htps:/]{7,8}.*?reddit.com/([user]{1,4})/([^/]+)", 0, RegexReturn.ListByMatch, EDP.ReturnValue) End Sub Friend Overrides Function GetInstance(ByVal What As Download) As IPluginContentProvider Select Case What @@ -55,19 +56,9 @@ Namespace API.Reddit End Select Return Nothing End Function - Private ReadOnly RedditRegEx1 As RParams = RParams.DMS("[htps:/]{7,8}.*?reddit.com/user/([^/]+)", 1) - Private ReadOnly RedditRegEx2 As RParams = RParams.DMS(".?u/([^/]+)", 1) - Private ReadOnly RedditChannelRegEx1 As RParams = RParams.DMS("[htps:/]{7,8}.*?reddit.com/r/([^/]+)", 1) - Private ReadOnly RedditChannelRegEx2 As RParams = RParams.DMS(".?r/([^/]+)", 1) Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions - Dim s$ - Dim c% = 0 - For Each r As RParams In {RedditRegEx1, RedditRegEx2, RedditChannelRegEx1, RedditChannelRegEx2} - s = RegexReplace(UserURL, r) - If Not s.IsEmptyString Then Return New ExchangeOptions(Site, s, c > 1) - c += 1 - Next - Return Nothing + Dim l As List(Of String) = RegexReplace(UserURL, UserRegex) + If l.ListExists(3) Then Return New ExchangeOptions(Site, l(2), l(1) = "r") Else Return Nothing End Function Friend Overrides Function Available(ByVal What As Download, ByVal Silent As Boolean) As Boolean Try @@ -83,7 +74,7 @@ Namespace API.Reddit avg.NumToString(New ANumbers With {.FormatOptions = ANumbers.Options.GroupIntegral}) & " outage reports:" & vbCr & dl.ListToString(vbCr) & vbCr & vbCr & "Do you want to continue parsing Reddit data?", "There are outage reports on Reddit"}, vbYesNo) = vbYes Then - DirectCast(Settings(RedGifs.RedGifsSiteKey).Source, RedGifs.SiteSettings).UpdateTokenIfRequired() + UpdateRedGifsToken() Return True Else Return False @@ -91,11 +82,15 @@ Namespace API.Reddit End If End If End If + UpdateRedGifsToken() Return True Catch ex As Exception Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, "[API.Reddit.SiteSettings.Available]", True) End Try End Function + Private Sub UpdateRedGifsToken() + DirectCast(Settings(RedGifs.RedGifsSiteKey).Source, RedGifs.SiteSettings).UpdateTokenIfRequired() + End Sub Friend Overrides Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable Dim spf$ = String.Empty Dim f As SFile = GetSpecialDataFile(Path, AskForPath, spf) @@ -108,8 +103,8 @@ Namespace API.Reddit Using f As New RedditViewSettingsForm(Options) : f.ShowDialog() : End Using End If End Sub - Friend Overrides Function GetUserPostUrl(ByVal UserID As String, ByVal PostID As String) As String - Return $"https://www.reddit.com/comments/{PostID.Split("_").LastOrDefault}/" + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + Return $"https://www.reddit.com/comments/{Media.Post.ID.Split("_").LastOrDefault}/" End Function End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Reddit/UserData.vb b/SCrawler/API/Reddit/UserData.vb index dc1284e..cb84692 100644 --- a/SCrawler/API/Reddit/UserData.vb +++ b/SCrawler/API/Reddit/UserData.vb @@ -14,8 +14,8 @@ Imports SCrawler.Plugin.Hosts Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.ImageRenderer -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UStates = SCrawler.API.Base.UserMedia.States Imports UTypes = SCrawler.API.Base.UserMedia.Types Imports CView = SCrawler.API.Reddit.IRedditView.View @@ -152,6 +152,8 @@ Namespace API.Reddit Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) _TotalPostsDownloaded = 0 If IsSavedPosts Then + 'TODO: Reddit saved posts: remove Unicode converter? + Responser.DecodersError = EDP.ReturnValue DownloadDataChannel(String.Empty, Token) ElseIf IsChannel Then If ChannelInfo Is Nothing Then diff --git a/SCrawler/API/Redgifs/Declarations.vb b/SCrawler/API/Redgifs/Declarations.vb index 1deff2c..3fd01e6 100644 --- a/SCrawler/API/Redgifs/Declarations.vb +++ b/SCrawler/API/Redgifs/Declarations.vb @@ -14,6 +14,6 @@ Namespace API.RedGifs Friend ReadOnly DateProvider As New CustomProvider(Function(v, d, p, n, e) ADateTime.ParseUnicode(v, n, e)) Friend ReadOnly WatchIDRegex As RParams = RParams.DMS(".+?watch/([^\?&""/]+)", 1, EDP.ReturnValue) Friend ReadOnly ThumbsIDRegex As RParams = RParams.DMS("([^/\?&""]+?)(-\w+?|)\.(mp4|jpg)", 1, EDP.ReturnValue, - Function(v) If(CStr(v).IsEmptyString, String.Empty, CStr(v).ToLower.Trim)) + CType(Function(Input$) Input.StringToLower.StringTrim, Func(Of String, String))) End Module End Namespace \ No newline at end of file diff --git a/SCrawler/API/Redgifs/SiteSettings.vb b/SCrawler/API/Redgifs/SiteSettings.vb index a963252..fc71a92 100644 --- a/SCrawler/API/Redgifs/SiteSettings.vb +++ b/SCrawler/API/Redgifs/SiteSettings.vb @@ -11,8 +11,9 @@ Imports SCrawler.Plugin Imports SCrawler.Plugin.Attributes Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Cookies +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UTypes = SCrawler.API.Base.UserMedia.Types Imports UStates = SCrawler.API.Base.UserMedia.States Namespace API.RedGifs @@ -40,11 +41,9 @@ Namespace API.RedGifs MyBase.New(RedGifsSite, "redgifs.com") Dim t$ = String.Empty With Responser - Dim b As Boolean = Not .UseWebClient Or Not .UseWebClientCookies Or Not .UseWebClientAdditionalHeaders - .UseWebClient = True - .UseWebClientCookies = True - .UseWebClientAdditionalHeaders = True - If .Headers.Count > 0 AndAlso .Headers.ContainsKey(TokenName) Then t = .Headers(TokenName) + Dim b As Boolean = Not .Mode = Response.Modes.WebClient + .Mode = Response.Modes.WebClient + t = .HeadersValue(TokenName) If b Then .SaveSettings() End With NoCredentialsResponser = New Response($"{SettingsFolderName}\Responser_{RedGifsSite}_NC.xml") With { @@ -68,10 +67,8 @@ Namespace API.RedGifs #End Region #Region "Response updater" Private Sub UpdateResponse(ByVal Value As String) - With Responser.Headers - If .Count = 0 OrElse Not .ContainsKey(TokenName) Then .Add(TokenName, Value) Else .Item(TokenName) = Value - Responser.SaveSettings() - End With + Responser.HeadersAdd(TokenName, Value) + Responser.SaveSettings() End Sub #End Region #Region "Token updaters" @@ -153,8 +150,8 @@ Namespace API.RedGifs End If Return Nothing End Function - Friend Overrides Function GetUserPostUrl(ByVal UserID As String, ByVal PostID As String) As String - Return $"https://www.redgifs.com/watch/{PostID}" + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + Return $"https://www.redgifs.com/watch/{Media.Post.ID}" End Function Friend Overrides Function BaseAuthExists() As Boolean Return UpdateTokenIfRequired() AndAlso ACheck(Token.Value) diff --git a/SCrawler/API/Redgifs/UserData.vb b/SCrawler/API/Redgifs/UserData.vb index 1042d77..02a2f61 100644 --- a/SCrawler/API/Redgifs/UserData.vb +++ b/SCrawler/API/Redgifs/UserData.vb @@ -11,8 +11,8 @@ Imports System.Threading Imports SCrawler.API.Base Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UTypes = SCrawler.API.Base.UserMedia.Types Imports UStates = SCrawler.API.Base.UserMedia.States Namespace API.RedGifs @@ -62,7 +62,7 @@ Namespace API.RedGifs Case DateResult.Exit : Exit Sub End Select postID = g.Value("id") - If Not _TempPostsList.Contains(postID) Then _TempPostsList.Add(postID) Else Exit For + If Not _TempPostsList.Contains(postID) Then _TempPostsList.Add(postID) Else Exit Sub ObtainMedia(g, postID, postDate) Next End If @@ -179,7 +179,7 @@ Namespace API.RedGifs If Host.Source.Available(Plugin.ISiteSettings.Download.Main, True) Then If Responser Is Nothing Then Responser = Host.Responser.Copy URL = String.Format(PostDataUrl, Obj.ToLower) - Dim r$ = Responser.DownloadString(URL, EDP.ThrowException) + Dim r$ = Responser.GetResponse(URL,, EDP.ThrowException) If Not r.IsEmptyString Then Using j As EContainer = JsonDocument.Parse(r) If Not j Is Nothing Then @@ -206,8 +206,15 @@ Namespace API.RedGifs If Not Responser Is Nothing AndAlso (Responser.Client.StatusCode = DataGone Or Responser.Client.StatusCode = HttpStatusCode.NotFound) Then Return New UserMedia With {.State = DataGone} Else - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[API.RedGifs.UserData.GetDataFromUrlId({URL})]", - New UserMedia With {.State = UStates.Missing}) + Dim m As New UserMedia With {.State = UStates.Missing} + Dim _errText$ = "API.RedGifs.UserData.GetDataFromUrlId({0})" + If Responser.Client.StatusCode = HttpStatusCode.Unauthorized Then + _errText = $"RedGifs credentials have expired [{CInt(Responser.Client.StatusCode)}]: {_errText}" + MyMainLOG = String.Format(_errText, URL) + Return m + Else + Return ErrorsDescriber.Execute(EDP.SendInLog, ex, String.Format(_errText, URL), m) + End If End If End Try End Function diff --git a/SCrawler/API/TikTok/UserData.vb b/SCrawler/API/TikTok/UserData.vb index a75048b..6f858bf 100644 --- a/SCrawler/API/TikTok/UserData.vb +++ b/SCrawler/API/TikTok/UserData.vb @@ -10,7 +10,7 @@ Imports System.Threading Imports SCrawler.API.Base Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB +Imports PersonalUtilities.Tools.Web.Clients Namespace API.TikTok Friend Class UserData : Inherits UserDataBase Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) diff --git a/SCrawler/API/Twitter/Declarations.vb b/SCrawler/API/Twitter/Declarations.vb index 5febb24..a1a055c 100644 --- a/SCrawler/API/Twitter/Declarations.vb +++ b/SCrawler/API/Twitter/Declarations.vb @@ -6,14 +6,21 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY +Imports System.Globalization Imports PersonalUtilities.Functions.XML.Base Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Twitter Friend Module Declarations Friend Const TwitterSite As String = "Twitter" - Friend DateProvider As New ADateTime(ADateTime.Formats.BaseDateTime) + Friend ReadOnly DateProvider As ADateTime = GetDateProvider() Friend ReadOnly VideoNode As NodeParams() = {New NodeParams("video_info", True, True, True, True, 10)} Friend ReadOnly VideoSizeRegEx As RParams = RParams.DMS("\d+x(\d+)", 1, EDP.ReturnValue) Friend ReadOnly UserIdRegEx As RParams = RParams.DMS("user_id.:.(\d+)", 1, EDP.ReturnValue) + Private Function GetDateProvider() As ADateTime + Dim n As DateTimeFormatInfo = CultureInfo.GetCultureInfo("en-us").DateTimeFormat.Clone + n.FullDateTimePattern = "ddd MMM dd HH:mm:ss +ffff yyyy" + n.TimeSeparator = String.Empty + Return New ADateTime(DirectCast(n.Clone, DateTimeFormatInfo)) With {.DateTimeStyle = DateTimeStyles.AssumeUniversal} + End Function End Module End Namespace \ No newline at end of file diff --git a/SCrawler/API/Twitter/SiteSettings.vb b/SCrawler/API/Twitter/SiteSettings.vb index cf2899d..6040603 100644 --- a/SCrawler/API/Twitter/SiteSettings.vb +++ b/SCrawler/API/Twitter/SiteSettings.vb @@ -9,8 +9,9 @@ Imports SCrawler.API.Base Imports SCrawler.Plugin Imports SCrawler.Plugin.Attributes -Imports PersonalUtilities.Tools.WEB Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Cookies Namespace API.Twitter Friend Class SiteSettings : Inherits SiteSettingsBase @@ -31,7 +32,7 @@ Namespace API.Twitter Private ReadOnly Property Auth As PropertyValue Private ReadOnly Property Token As PropertyValue - + Friend ReadOnly Property SavedPostsUserName As PropertyValue Friend Overrides ReadOnly Property Responser As Response Friend Sub New() @@ -45,10 +46,8 @@ Namespace API.Twitter If .File.Exists Then If EncryptCookies.CookiesEncrypted Then .CookiesEncryptKey = SettingsCLS.CookieEncryptKey .LoadSettings() - With .Headers - If .ContainsKey(Header_Authorization) Then a = .Item(Header_Authorization) - If .ContainsKey(Header_Token) Then t = .Item(Header_Token) - End With + a = .HeadersValue(Header_Authorization) + t = .HeadersValue(Header_Token) Else .ContentType = "application/json" .Accept = "*/*" @@ -56,17 +55,15 @@ Namespace API.Twitter .Cookies = New CookieKeeper(.CookiesDomain) With {.EncryptKey = SettingsCLS.CookieEncryptKey} .CookiesEncryptKey = SettingsCLS.CookieEncryptKey .Decoders.Add(SymbolsConverter.Converters.Unicode) - With .Headers - .Add("sec-ch-ua", " Not;A Brand"";v=""99"", ""Google Chrome"";v=""91"", ""Chromium"";v=""91""") - .Add("sec-ch-ua-mobile", "?0") - .Add("sec-fetch-dest", "empty") - .Add("sec-fetch-mode", "cors") - .Add("sec-fetch-site", "same-origin") - .Add(Header_Token, String.Empty) - .Add("x-twitter-active-user", "yes") - .Add("x-twitter-auth-type", "OAuth2Session") - .Add(Header_Authorization, String.Empty) - End With + .HeadersAdd("sec-ch-ua", " Not;A Brand"";v=""99"", ""Google Chrome"";v=""91"", ""Chromium"";v=""91""") + .HeadersAdd("sec-ch-ua-mobile", "?0") + .HeadersAdd("sec-fetch-dest", "empty") + .HeadersAdd("sec-fetch-mode", "cors") + .HeadersAdd("sec-fetch-site", "same-origin") + .HeadersAdd(Header_Token, String.Empty) + .HeadersAdd("x-twitter-active-user", "yes") + .HeadersAdd("x-twitter-auth-type", "OAuth2Session") + .HeadersAdd(Header_Authorization, String.Empty) .SaveSettings() End If End With @@ -87,8 +84,8 @@ Namespace API.Twitter Case NameOf(Token) : f = Header_Token End Select If Not f.IsEmptyString Then - If Responser.Headers.Count > 0 AndAlso Responser.Headers.ContainsKey(f) Then Responser.Headers.Remove(f) - If Not CStr(Value).IsEmptyString Then Responser.Headers.Add(f, CStr(Value)) + Responser.HeadersRemove(f) + If Not CStr(Value).IsEmptyString Then Responser.HeadersAdd(f, CStr(Value)) Responser.SaveSettings() End If End If @@ -103,8 +100,8 @@ Namespace API.Twitter Friend Overrides Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable Return UserData.GetVideoInfo(URL, Responser) End Function - Friend Overrides Function GetUserPostUrl(ByVal UserID As String, ByVal PostID As String) As String - Return $"https://twitter.com/{UserID}/status/{PostID}" + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + Return $"https://twitter.com/{User.Name}/status/{Media.Post.ID}" End Function Friend Overrides Function BaseAuthExists() As Boolean Return If(Responser.Cookies?.Count, 0) > 0 And ACheck(Token.Value) And ACheck(Auth.Value) diff --git a/SCrawler/API/Twitter/UserData.vb b/SCrawler/API/Twitter/UserData.vb index 605de2e..3da25eb 100644 --- a/SCrawler/API/Twitter/UserData.vb +++ b/SCrawler/API/Twitter/UserData.vb @@ -11,8 +11,8 @@ Imports System.Threading Imports SCrawler.API.Base Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UStates = SCrawler.API.Base.UserMedia.States Namespace API.Twitter Friend Class UserData : Inherits UserDataBase diff --git a/SCrawler/API/UserDataBind.vb b/SCrawler/API/UserDataBind.vb index 8c4058a..4541b06 100644 --- a/SCrawler/API/UserDataBind.vb +++ b/SCrawler/API/UserDataBind.vb @@ -8,9 +8,10 @@ ' but WITHOUT ANY WARRANTY Imports System.Threading Imports SCrawler.API.Base -Imports PersonalUtilities.Tools +Imports PersonalUtilities.Forms Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.Messaging +Imports PersonalUtilities.Tools Namespace API Friend Class UserDataBind : Inherits UserDataBase : Implements ICollection(Of IUserData), IMyEnumerator(Of IUserData) #Region "Events" @@ -20,6 +21,17 @@ Namespace API #Region "Declarations" Friend ReadOnly Property Collections As List(Of IUserData) #Region "Base class overrides" + Friend Overrides ReadOnly Property IsVirtual As Boolean + Get + Return CollectionModel = UsageModel.Virtual + End Get + End Property + Friend Overrides ReadOnly Property CollectionModel As UsageModel + Get + If Count > 0 Then Return Item(0).CollectionModel Else Return UsageModel.Default + End Get + End Property + Friend Property CurrentlyEdited As Boolean = False Private _CollectionName As String = String.Empty Friend Overrides Property CollectionName As String Get @@ -80,10 +92,13 @@ Namespace API End Sub Friend Overrides Function GetUserPicture() As Image If Count > 0 Then - Return Collections(0).GetPicture - Else - Return GetNullPicture(Settings.MaxLargeImageHeight) + Dim img As Image + For Each u As UserDataBase In Collections + img = u.GetPicture(Of Image)(False) + If Not img Is Nothing Then Return img + Next End If + Return GetNullPicture(Settings.MaxLargeImageHeight) End Function #End Region Friend Overrides ReadOnly Property DownloadedTotal(Optional ByVal Total As Boolean = True) As Integer @@ -102,7 +117,15 @@ Namespace API End Property Friend Overrides Property MyFile As SFile Get - If Count > 0 Then Return Collections(0).File Else Return Nothing + If Count > 0 Then + If IsVirtual Then + Return GetRealUserFile.IfNullOrEmpty(Collections(0).File) + Else + Return Collections(0).File + End If + Else + Return Nothing + End If End Get Set(ByVal NewFile As SFile) End Set @@ -120,8 +143,8 @@ Namespace API End Property Friend Overrides Property DataMerging As Boolean Get - If Count > 0 Then - Return DirectCast(Collections(0), UserDataBase).DataMerging + If Count > 0 AndAlso Collections.Exists(RealUser) Then + Return DirectCast(Collections.Find(RealUser), UserDataBase).DataMerging Else Return False End If @@ -184,6 +207,7 @@ Namespace API End Property Friend Overrides Function GetUserInformation() As String Dim OutStr$ = String.Empty + If IsVirtual Then OutStr = "This is a virtual collection." If Count > 0 Then Collections.ForEach(Sub(c) OutStr.StringAppendLine(DirectCast(c, UserDataBase).GetUserInformation(), vbNewLine.StringDup(2))) Return OutStr End Function @@ -346,12 +370,36 @@ Namespace API If Not e.Exists Then e = New ErrorsDescriber(EDP.SendInLog) If Count > 0 Then Collections.ForEach(Sub(c) c.OpenSite(e)) End Sub + Private ReadOnly RealUser As Predicate(Of IUserData) = Function(u) u.UserModel = UsageModel.Default Friend Overrides Sub OpenFolder() Try - If Count > 0 Then GlobalOpenPath(Collections(0).File.CutPath(2)) + If Count > 0 Then + Dim i% = Collections.FindIndex(RealUser) + If i = -1 Then i = 0 + If i >= 0 Then + If IsVirtual Or Collections(i).UserModel = UsageModel.Virtual Then + Collections(i).OpenFolder() + Else + GlobalOpenPath(Collections(i).File.CutPath(2)) + End If + End If + End If Catch End Try End Sub + Friend Function GetRealUserFile() As SFile + Dim i% = -1 + If Count > 0 Then i = Collections.FindIndex(RealUser) + If i >= 0 Then Return Collections(i).File Else Return Nothing + End Function + Friend Function GetRealUserSpecialCollectionPath() + Dim _SpecialCollectionPath As SFile = Nothing + If Count > 0 And Not IsVirtual Then + Dim _RealUser As UserDataBase = Collections.Find(RealUser) + If Not _RealUser Is Nothing Then _SpecialCollectionPath = _RealUser.User.SpecialCollectionPath + End If + Return _SpecialCollectionPath + End Function #End Region #Region "ICollection Support" Private ReadOnly Property IsReadOnly As Boolean Implements ICollection(Of IUserData).IsReadOnly @@ -386,8 +434,8 @@ Namespace API ''' Friend Overloads Sub Add(ByVal _Item As IUserData) Implements ICollection(Of IUserData).Add With _Item - If .MoveFiles(CollectionName) Then - If DataMerging Then DirectCast(.Self, UserDataBase).MergeData() + If .MoveFiles(CollectionName, GetRealUserSpecialCollectionPath()) Then + If Not _Item.IsVirtual And DataMerging Then DirectCast(.Self, UserDataBase).MergeData() Collections.Add(_Item) With Collections.Last If Count > 1 Then @@ -445,14 +493,9 @@ Namespace API Private Sub ConsolidateScripts() If Count > 1 AndAlso ScriptUse Then Collections.ForEach(Sub(c) c.ScriptUse = True) End Sub - Friend Sub AddRange(ByVal _Items As IEnumerable(Of IUserData)) - If _Items.ListExists Then - For i% = 0 To _Items.Count - 1 : Add(_Items(i)) : Next - End If - End Sub #End Region #Region "Move, Merge" - Friend Overrides Function MoveFiles(ByVal __CollectionName As String) As Boolean + Friend Overrides Function MoveFiles(ByVal __CollectionName As String, ByVal __SpecialCollectionPath As SFile) As Boolean Throw New NotImplementedException("Move files is not available in the collection context") End Function Friend Overloads Sub MergeData(ByVal Merging As Boolean) @@ -488,52 +531,69 @@ Namespace API "Operation canceled", MsgBoxStyle.Critical) Return False Else - DirectCast(_Item, UserDataBase).MoveFiles(String.Empty) + _Item.MoveFiles(String.Empty, Nothing) MainFrameObj.ImageHandler(_Item) AddRemoveBttDeleteHandler(_Item, False) RaiseEvent OnUserRemoved(_Item) Return Collections.Remove(_Item) End If End Function - Friend Overrides Function Delete(Optional ByVal Multiple As Boolean = False) As Integer + Friend Overrides Function Delete(Optional ByVal Multiple As Boolean = False, Optional ByVal CollectionValue As Integer = -1) As Integer If Count > 0 Then Const MsgTitle$ = "Deleting a collection" - Dim f As SFile + Dim f As SFile = Nothing + If Not IsVirtual Then + f = GetRealUserFile() + If Not f.IsEmptyString Then f = f.CutPath(IIf(DataMerging, 1, 2)) + End If Dim m As New MMessage($"Collection [{CollectionName} (number of profiles: {Count})] may contain data" & vbCr & "Are you sure you want to delete the collection and all of its files?", MsgTitle, - {New MsgBoxButton("Delete") With {.ToolTip = "Delete the collection and all files"}, + {New MsgBoxButton("Delete") With {.ToolTip = "Delete the collection and all files", .KeyCode = Keys.Enter}, New MsgBoxButton("Split") With { .ToolTip = "Users will be removed from the collection and will be displayed in the program as separate users." & vbCr & - "All user data will remain."}, + "All user data will remain.", + .KeyCode = New ButtonKey(Keys.Enter, True)}, "Cancel"}, vbExclamation) - Select Case If(Multiple, 0, MsgBoxE(m).Index) + Dim v% + If CollectionValue >= 0 Then + v = CollectionValue + ElseIf Multiple Then + v = 0 + Else + v = MsgBoxE(m) + End If + Select Case v Case 0 - f = Collections(0).File.CutPath(IIf(DataMerging, 1, 2)).PathWithSeparator - Settings.Users.Remove(Me) Collections.ForEach(Sub(c) c.Delete()) - Downloader.UserRemove(Me) - MainFrameObj.ImageHandler(Me, False) - Collections.ListClearDispose - Dispose(False) - f.Delete(SFO.Path, SFODelete.EmptyOnly + Settings.DeleteMode, EDP.SendInLog) - Return 2 + If Collections.All(Function(c As UserDataBase) c.Disposed) Then + Settings.Users.Remove(Me) + Downloader.UserRemove(Me) + MainFrameObj.ImageHandler(Me, False) + Collections.ListClearDispose + Dispose(False) + If Not f.IsEmptyString Then f.Delete(SFO.Path, SFODelete.EmptyOnly + Settings.DeleteMode, EDP.SendInLog) + Return 2 + End If Case 1 If DataMerging Then MsgBoxE({$"Collection [{CollectionName}] data merged{vbCr}Unable to split merged collection{vbCr}Operation canceled", MsgTitle}, vbExclamation) Return 0 Else - f = Collections(0).File.CutPath(2) - Settings.Users.Remove(Me) - Collections.ForEach(Sub(c) - c.MoveFiles(String.Empty) - MainFrameObj.ImageHandler(c) + Collections.ForEach(Sub(ByVal c As IUserData) + If c.MoveFiles(String.Empty, Nothing) Then + UserListLoader.UpdateUser(Settings.GetUser(c), True) + MainFrameObj.ImageHandler(c) + End If End Sub) - Collections.Clear() - f.Delete(SFO.Path, SFODelete.Default + Settings.DeleteMode, EDP.SendInLog) - Downloader.UserRemove(Me) - MainFrameObj.ImageHandler(Me, False) - Dispose(False) - Return 3 + If Collections.All(Function(c) c.CollectionName.IsEmptyString) Then + Settings.Users.Remove(Me) + Collections.Clear() + If Not f.IsEmptyString Then f.Delete(SFO.Path, SFODelete.Default + Settings.DeleteMode, EDP.SendInLog) + Downloader.UserRemove(Me) + MainFrameObj.ImageHandler(Me, False) + Dispose(False) + Return 3 + End If End If Case Else : If Not Multiple Then MsgBoxE({"Operation canceled", MsgTitle}) End Select @@ -562,9 +622,11 @@ Namespace API "Deleting a user"}, vbExclamation,,, { New MsgBoxButton("Remove") With { - .ToolTip = "Remove a user from the collection only. All its data will remain. The user will appear in the program."}, + .ToolTip = "Remove a user from the collection only. All its data will remain. The user will appear in the program.", + .KeyCode = Keys.Enter}, New MsgBoxButton("Delete") With { - .ToolTip = "Delete a user from the collection and erase their data."}, + .ToolTip = "Delete a user from the collection and erase their data.", + .KeyCode = New ButtonKey(Keys.Enter, True)}, "Cancel" }).Index Case 0 diff --git a/SCrawler/API/XVIDEOS/Declarations.vb b/SCrawler/API/XVIDEOS/Declarations.vb index cbd310f..dcbdc58 100644 --- a/SCrawler/API/XVIDEOS/Declarations.vb +++ b/SCrawler/API/XVIDEOS/Declarations.vb @@ -10,10 +10,13 @@ Imports PersonalUtilities.Functions.RegularExpressions Namespace API.XVIDEOS Friend Module Declarations Friend Const XvideosSiteKey As String = "AndyProgram_XVIDEOS" - Friend ReadOnly Property M3U8Regex As RParams = RParams.DM("http.+?.m3u8.*?(?=')", 0) - Friend ReadOnly Property VideoTitleRegex As RParams = RParams.DMS("html5player.setVideoTitle\('(.+)(?='\);)", 1) - Friend ReadOnly Property VideoID As RParams = RParams.DMS(".*?www.xvideos.com/(video\d+).*", 1) - Friend ReadOnly Property M3U8Reparse As RParams = RParams.DM("NAME=""(\d+).*?""[\r\n]*?(.+)(?=(|[\r\n]+?))", 0, RegexReturn.List) - Friend ReadOnly Property M3U8Appender As RParams = RParams.DM("(.+)(?=/.+?\.m3u8.*?)", 0) + Private ReadOnly HtmlConverter As Func(Of String, String) = Function(Input) SymbolsConverter.HTML.Decode(Input, EDP.ReturnValue) + Friend ReadOnly Regex_M3U8 As RParams = RParams.DM("http.+?.m3u8.*?(?=')", 0) + Friend ReadOnly Regex_VideoTitle As RParams = RParams.DMS("html5player.setVideoTitle\('(.+)(?='\);)", 1, EDP.ReturnValue, HtmlConverter) + Friend ReadOnly Regex_VideoID As RParams = RParams.DMS(".*?www.xvideos.com/(video\d+).*", 1) + Friend ReadOnly Regex_M3U8_Reparse As RParams = RParams.DM("NAME=""(\d+).*?""[\r\n]*?(.+)(?=(|[\r\n]+?))", 0, RegexReturn.List) + Friend ReadOnly Regex_M3U8_Appender As RParams = RParams.DM("(.+)(?=/.+?\.m3u8.*?)", 0) + Friend ReadOnly Regex_SavedVideosPlaylist As RParams = RParams.DM("
    - Partial Friend Class SettingsForm : Inherits System.Windows.Forms.Form - - Protected Overrides Sub Dispose(ByVal disposing As Boolean) - Try - If disposing AndAlso components IsNot Nothing Then - components.Dispose() - End If - Finally - MyBase.Dispose(disposing) - End Try - End Sub - Private components As System.ComponentModel.IContainer - - Private Sub InitializeComponent() - Dim CONTAINER_MAIN As System.Windows.Forms.ToolStripContainer - Me.LIST_DOMAINS = New System.Windows.Forms.ListBox() - CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() - CONTAINER_MAIN.ContentPanel.SuspendLayout() - CONTAINER_MAIN.SuspendLayout() - Me.SuspendLayout() - ' - 'CONTAINER_MAIN - ' - ' - 'CONTAINER_MAIN.ContentPanel - ' - CONTAINER_MAIN.ContentPanel.Controls.Add(Me.LIST_DOMAINS) - CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(384, 241) - CONTAINER_MAIN.Dock = System.Windows.Forms.DockStyle.Fill - CONTAINER_MAIN.LeftToolStripPanelVisible = False - CONTAINER_MAIN.Location = New System.Drawing.Point(0, 0) - CONTAINER_MAIN.Name = "CONTAINER_MAIN" - CONTAINER_MAIN.RightToolStripPanelVisible = False - CONTAINER_MAIN.Size = New System.Drawing.Size(384, 291) - CONTAINER_MAIN.TabIndex = 0 - ' - 'LIST_DOMAINS - ' - Me.LIST_DOMAINS.Dock = System.Windows.Forms.DockStyle.Fill - Me.LIST_DOMAINS.FormattingEnabled = True - Me.LIST_DOMAINS.Location = New System.Drawing.Point(0, 0) - Me.LIST_DOMAINS.Name = "LIST_DOMAINS" - Me.LIST_DOMAINS.Size = New System.Drawing.Size(384, 241) - Me.LIST_DOMAINS.TabIndex = 0 - ' - 'SettingsForm - ' - Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) - Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font - Me.ClientSize = New System.Drawing.Size(384, 291) - Me.Controls.Add(CONTAINER_MAIN) - Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle - Me.Icon = Global.SCrawler.My.Resources.SiteResources.XvideosIcon_48 - Me.KeyPreview = True - Me.MaximizeBox = False - Me.MaximumSize = New System.Drawing.Size(400, 330) - Me.MinimizeBox = False - Me.MinimumSize = New System.Drawing.Size(400, 330) - Me.Name = "SettingsForm" - Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide - Me.Text = "Settings" - CONTAINER_MAIN.ContentPanel.ResumeLayout(False) - CONTAINER_MAIN.ResumeLayout(False) - CONTAINER_MAIN.PerformLayout() - Me.ResumeLayout(False) - - End Sub - Private WithEvents LIST_DOMAINS As Windows.Forms.ListBox - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/XVIDEOS/SettingsForm.vb b/SCrawler/API/XVIDEOS/SettingsForm.vb deleted file mode 100644 index be8ed58..0000000 --- a/SCrawler/API/XVIDEOS/SettingsForm.vb +++ /dev/null @@ -1,70 +0,0 @@ -' 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 PersonalUtilities.Forms -Imports PersonalUtilities.Forms.Toolbars -Namespace API.XVIDEOS - Friend Class SettingsForm - Private Const SettingsDesignXmlNode As String = "XvideosSettingsForm" - Private WithEvents MyDefs As DefaultFormOptions - Private ReadOnly Property Source As SiteSettings - Friend Sub New(ByRef s As SiteSettings) - InitializeComponent() - Source = s - MyDefs = New DefaultFormOptions(Me, Settings.Design) - End Sub - Private Sub SettingsForm_Load(sender As Object, e As EventArgs) Handles Me.Load - With MyDefs - If Not Settings.Design.Contains(SettingsDesignXmlNode) Then Settings.Design.Add(SettingsDesignXmlNode, String.Empty) - .MyViewInitialize(Me, Settings.Design(SettingsDesignXmlNode), True) - .AddEditToolbar() - .AddOkCancelToolbar() - If Source.Domains.Count > 0 Then Source.Domains.ForEach(Sub(d) LIST_DOMAINS.Items.Add(d)) - .EndLoaderOperations() - End With - End Sub - Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick - Source.Domains.Clear() - With LIST_DOMAINS - If .Items.Count > 0 Then - For Each i In .Items : Source.Domains.Add(i.ToString) : Next - End If - End With - Source.UpdateDomains() - MyDefs.CloseForm() - End Sub - Private Sub MyDefs_ButtonAddClick(ByVal Sender As Object, ByVal e As EditToolbarEventArgs) Handles MyDefs.ButtonAddClick - Dim nd$ = InputBoxE("Enter a new domain using the pattern [xvideos.com]:", "New domain") - If Not nd.IsEmptyString Then - If Not LIST_DOMAINS.Items.Contains(nd) Then - LIST_DOMAINS.Items.Add(nd) - Else - MsgBoxE($"The domain [{nd}] already added") - End If - End If - End Sub - Private Sub MyDefs_ButtonDeleteClickE(ByVal Sender As Object, ByVal e As EditToolbarEventArgs) Handles MyDefs.ButtonDeleteClickE - Const MsgTitle$ = "Removing domains" - If _LatestSelected.ValueBetween(0, LIST_DOMAINS.Items.Count - 1) Then - Dim n$ = LIST_DOMAINS.Items(_LatestSelected) - If MsgBoxE({$"Are you sure you want to delete the [{n}] domain?", MsgTitle}, MsgBoxStyle.YesNo) = MsgBoxResult.Yes Then - LIST_DOMAINS.Items.RemoveAt(_LatestSelected) - MsgBoxE({$"Domain [{n}] removed", MsgTitle}) - Else - MsgBoxE({"Operation canceled", MsgTitle}) - End If - Else - MsgBoxE({"No domain selected", MsgTitle}, vbExclamation) - End If - End Sub - Private _LatestSelected As Integer = -1 - Private Sub LIST_DOMENS_SelectedIndexChanged(sender As Object, e As EventArgs) Handles LIST_DOMAINS.SelectedIndexChanged - _LatestSelected = LIST_DOMAINS.SelectedIndex - End Sub - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/XVIDEOS/SiteSettings.vb b/SCrawler/API/XVIDEOS/SiteSettings.vb index 0c1103a..cbd691e 100644 --- a/SCrawler/API/XVIDEOS/SiteSettings.vb +++ b/SCrawler/API/XVIDEOS/SiteSettings.vb @@ -7,15 +7,16 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports SCrawler.API.Base +Imports SCrawler.API.BaseObjects Imports SCrawler.Plugin Imports SCrawler.Plugin.Attributes Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB +Imports PersonalUtilities.Tools.Web.Clients Namespace API.XVIDEOS - - Friend Class SiteSettings : Inherits SiteSettingsBase -#Region "Images" - Friend Overrides ReadOnly Property Icon As Icon + + Friend Class SiteSettings : Inherits SiteSettingsBase : Implements IDomainContainer +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon Implements IDomainContainer.Icon Get Return My.Resources.SiteResources.XvideosIcon_48 End Get @@ -25,58 +26,87 @@ Namespace API.XVIDEOS Return My.Resources.SiteResources.XvideosPic_32 End Get End Property +#Region "Domains" + Private ReadOnly Property IDomainContainer_Site As String Implements IDomainContainer.Site + Get + Return Site + End Get + End Property + Private ReadOnly Property SiteDomains As PropertyValue Implements IDomainContainer.DomainsSettingProp + Friend ReadOnly Property Domains As List(Of String) Implements IDomainContainer.Domains + Private ReadOnly Property DomainsTemp As List(Of String) Implements IDomainContainer.DomainsTemp + Private Property DomainsChanged As Boolean = False Implements IDomainContainer.DomainsChanged + Private ReadOnly Property DomainsDefault As String = "xvideos.com|xnxx.com" Implements IDomainContainer.DomainsDefault #End Region -#Region "Declarations" - Private Property SiteDomains As PropertyValue - Public Property DownloadUHD As PropertyValue - Friend ReadOnly Property Domains As List(Of String) - Private Const DomainsDefault As String = "xvideos.com|xnxx.com" - Private _Initialized As Boolean = False + Friend Property DownloadUHD As PropertyValue + Private Property Initialized As Boolean = False Implements IDomainContainer.Initialized + + Friend ReadOnly Property SavedVideosPlaylist As PropertyValue #End Region #Region "Initializer" Friend Sub New() MyBase.New("XVIDEOS", "www.xvideos.com") + Responser.DeclaredError = EDP.ThrowException Domains = New List(Of String) + DomainsTemp = New List(Of String) SiteDomains = New PropertyValue(DomainsDefault, GetType(String), Sub(s) UpdateDomains()) DownloadUHD = New PropertyValue(False) + SavedVideosPlaylist = New PropertyValue(String.Empty, GetType(String)) End Sub Friend Overrides Sub EndInit() - _Initialized = True - UpdateDomains() + Initialized = True + DomainContainer.EndInit(Me) + DomainsTemp.ListAddList(Domains) End Sub #End Region -#Region "Update" - Private _DomainsUpdateInProgress As Boolean = False - Friend Sub UpdateDomains() - If Not _Initialized Then Exit Sub - If Not _DomainsUpdateInProgress Then - _DomainsUpdateInProgress = True - If Not ACheck(SiteDomains.Value) Then SiteDomains.Value = DomainsDefault - Domains.ListAddList(CStr(SiteDomains.Value).Split("|"), LAP.NotContainsOnly, LAP.ClearBeforeAdd) - Domains.ListAddList(DomainsDefault.Split("|"), LAP.NotContainsOnly) - SiteDomains.Value = Domains.ListToString("|") - _DomainsUpdateInProgress = False - End If +#Region "Edit" + Private Property DomainsUpdateInProgress As Boolean = False Implements IDomainContainer.DomainsUpdateInProgress + Private Property DomainsUpdatedBySite As Boolean = False Implements IDomainContainer.DomainsUpdatedBySite + Friend Sub UpdateDomains() Implements IDomainContainer.UpdateDomains + DomainContainer.UpdateDomains(Me) End Sub Friend Overrides Sub Update() - UpdateDomains() + DomainContainer.Update(Me) Responser.SaveSettings() End Sub + Friend Overrides Sub EndEdit() + DomainContainer.EndEdit(Me) + MyBase.EndEdit() + End Sub + Friend Overrides Sub OpenSettingsForm() + DomainContainer.OpenSettingsForm(Me) + End Sub #End Region #Region "Download" Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider - Return New UserData + If What = ISiteSettings.Download.SavedPosts Then + Return New UserData With {.IsSavedPosts = True, .User = New UserInfo With {.Name = "XVIDEOS"}} + Else + Return New UserData + End If End Function Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean - Return Settings.UseM3U8 + If Settings.UseM3U8 Then + If What = ISiteSettings.Download.SavedPosts Then + Return ACheck(SavedVideosPlaylist.Value) And If(Responser.Cookies?.Count, 0) > 0 + Else + Return True + End If + Else + Return False + End If End Function #End Region #Region "User: get, check" - Friend Overrides Function GetUserUrl(ByVal UserName As String, ByVal Channel As Boolean) As String - Dim user$ = UserName.Split("_").FirstOrDefault - user &= $"/{UserName.Replace($"{user}_", String.Empty)}" - Return user + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String + Dim __user$ = User.Name.Split("_").FirstOrDefault + __user &= $"/{User.Name.Replace($"{User}_", String.Empty)}" + Return __user End Function Private Const UserRegexDefault As String = "/(profiles|[\w]*?[-]{0,1}channels)/([^/]+)(\Z|.*?)" Private Const URD As String = ".*?{0}{1}" @@ -84,9 +114,10 @@ Namespace API.XVIDEOS If Not UserURL.IsEmptyString Then If Domains.Count > 0 Then Dim uName$, uOpt$, fStr$ + Dim uErr As New ErrorsDescriber(EDP.ReturnValue) For i% = 0 To Domains.Count - 1 fStr = String.Format(URD, Domains(i), UserRegexDefault) - uName = RegexReplace(UserURL, RParams.DMS(fStr, 2)) + uName = RegexReplace(UserURL, RParams.DMS(fStr, 2, uErr)) If Not uName.IsEmptyString Then uOpt = RegexReplace(UserURL, RParams.DMS(fStr, 1)) If Not uOpt.IsEmptyString Then Return New ExchangeOptions(Site, $"{uOpt}_{uName}") @@ -97,11 +128,6 @@ Namespace API.XVIDEOS Return Nothing End Function #End Region -#Region "Settings" - Friend Overrides Sub OpenSettingsForm() - Using f As New SettingsForm(Me) : f.ShowDialog() : End Using - End Sub -#End Region #Region "Get special data" Friend Overrides Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions If Not URL.IsEmptyString And Domains.Count > 0 Then diff --git a/SCrawler/API/XVIDEOS/UserData.vb b/SCrawler/API/XVIDEOS/UserData.vb index 93260f5..cf4c952 100644 --- a/SCrawler/API/XVIDEOS/UserData.vb +++ b/SCrawler/API/XVIDEOS/UserData.vb @@ -10,12 +10,29 @@ Imports System.Threading Imports SCrawler.API.Base Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.WEB -Imports PersonalUtilities.Tools.WebDocuments.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UStates = SCrawler.API.Base.UserMedia.States Imports UTypes = SCrawler.API.Base.UserMedia.Types Namespace API.XVIDEOS Friend Class UserData : Inherits UserDataBase + Private Structure PlayListVideo : Implements IRegExCreator + Friend ID As String + Friend URL As String + Friend Title As String + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists(3) Then + ID = ParamsArray(0) + URL = ParamsArray(1) + If Not URL.IsEmptyString Then URL = $"https://www.xvideos.com/{URL.StringTrimStart("/")}" + Title = ParamsArray(2).StringRemoveWinForbiddenSymbols.StringTrim + End If + Return Me + End Function + Friend Function ToUserMedia() As UserMedia + Return New UserMedia(URL, UTypes.VideoPre) With {.Object = Me, .PictureOption = Title, .Post = ID} + End Function + End Structure Private ReadOnly Property MySettings As SiteSettings Get Return DirectCast(HOST.Source, SiteSettings) @@ -28,71 +45,74 @@ Namespace API.XVIDEOS UseInternalM3U8Function = True End Sub Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + If Not Settings.UseM3U8 Then + If Not Settings.OS64 Then + MyMainLOG = $"XVIDEOS [{ToStringForLog()}]: The plugin only works with x64 OS." + Else + MyMainLOG = $"{ToStringForLog()}: File [ffmpeg.exe] not found" + End If + Exit Sub + End If + If IsSavedPosts Then + If Not ACheck(MySettings.SavedVideosPlaylist.Value) Then Throw New ArgumentNullException("SavedVideosPlaylist", "Playlist of saved videos cannot be null") + DownloadSavedVideos(Token) + Else + DownloadUserVideo(Token) + End If + End Sub + Private Sub DownloadUserVideo(ByVal Token As CancellationToken) Dim URL$ = String.Empty Try - If Not Settings.UseM3U8 Then - If Not Settings.OS64 Then - MyMainLOG = $"XVIDEOS [{ToStringForLog()}]: The plugin only works with x64 OS." - Else - MyMainLOG = $"{ToStringForLog()}: File [ffmpeg.exe] not found" - End If - Exit Sub - End If - Dim NextPage% = 0 Dim r$ + Dim j As EContainer = Nothing Dim jj As EContainer - Dim e As ErrorsDescriber = EDP.ThrowException - Dim user$ = MySettings.GetUserUrl(Name, False) + Dim user$ = MySettings.GetUserUrl(Me, False) Dim p As UserMedia Dim EnvirSet As Boolean = False Do ThrowAny(Token) URL = $"https://www.xvideos.com/{user}/videos/new/{If(NextPage = 0, String.Empty, NextPage)}" - r = Responser.GetResponse(URL,, e) + r = Responser.GetResponse(URL) If Not r.IsEmptyString Then If Not EnvirSet Then UserExists = True : UserSuspended = False : EnvirSet = True - With JsonDocument.Parse(r).XmlIfNothing + j = JsonDocument.Parse(r).XmlIfNothing + With j If .Contains("videos") Then With .Item("videos") If .Count > 0 Then NextPage += 1 For Each jj In .Self p = New UserMedia With { - .Post = New UserPost With {.ID = jj.Value("id")}, - .URL = $"https://www.xvideos.com{jj.Value("u")}" + .Post = jj.Value("id"), + .URL = $"https://www.xvideos.com/{jj.Value("u").StringTrimStart("/")}" } If Not p.Post.ID.IsEmptyString And Not jj.Value("u").IsEmptyString Then If Not _TempPostsList.Contains(p.Post.ID) Then _TempPostsList.Add(p.Post.ID) _TempMediaList.Add(p) Else - .Dispose() Exit Do End If End If Next - Else - .Dispose() - Exit Do + Continue Do End If End With - Else - .Dispose() - Exit Do End If - .Dispose() End With - Else - Exit Do End If - Loop + If Not j Is Nothing Then j.Dispose() + Exit Do + Loop While NextPage < 100 + + If Not j Is Nothing Then j.Dispose() If _TempMediaList.Count > 0 Then For i% = 0 To _TempMediaList.Count - 1 ThrowAny(Token) - With _TempMediaList(i) : _TempMediaList(i) = GetVideoData(.URL, Responser, MySettings.DownloadUHD.Value, .Post.ID) : End With + _TempMediaList(i) = GetVideoData(_TempMediaList(i), Responser, MySettings.DownloadUHD.Value) Next _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) End If @@ -108,38 +128,80 @@ Namespace API.XVIDEOS If _TempMediaList.ListExists Then _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) End Try End Sub - Private Function GetVideoData(ByVal URL As String, ByVal resp As Response, ByVal DownloadUHD As Boolean, ByVal ID As String) As UserMedia + Private Sub DownloadSavedVideos(ByVal Token As CancellationToken) + Dim URL$ = MySettings.SavedVideosPlaylist.Value Try - If Not URL.IsEmptyString Then - Dim r$ = resp.GetResponse(URL,, EDP.ThrowException) + Dim NextPage% = 0 + Dim __continue As Boolean = True + Dim r$ + Dim data As List(Of PlayListVideo) + Dim i% + Do + ThrowAny(Token) + URL = $"{MySettings.SavedVideosPlaylist.Value}{If(NextPage = 0, String.Empty, $"/{NextPage}")}" + r = Responser.GetResponse(URL,, EDP.ReturnValue) + If Responser.HasError Then + If Responser.StatusCode = Net.HttpStatusCode.NotFound And NextPage > 0 Then Exit Do + Throw New Exception(Responser.ErrorText, Responser.ErrorException) + End If + NextPage += 1 If Not r.IsEmptyString Then - Dim m$ = RegexReplace(r, M3U8Regex) - If Not m.IsEmptyString Then - Dim appender$ = RegexReplace(m, M3U8Appender) - Dim t$ = RegexReplace(r, VideoTitleRegex) - r = resp.GetResponse(m,, EDP.ThrowException) + data = RegexFields(Of PlayListVideo)(r, {Regex_SavedVideosPlaylist}, {1, 2, 3}, EDP.ReturnValue) + If data.ListExists Then + If data.RemoveAll(Function(d) _TempPostsList.Contains(d.ID)) > 0 Then __continue = False + If data.ListExists Then + _TempPostsList.ListAddList(data.Select(Function(d) d.ID), LNC) + i = _TempMediaList.Count + _TempMediaList.ListAddList(data.Select(Function(d) d.ToUserMedia()), LNC) + If _TempMediaList.Count = i Or Not __continue Then Exit Do Else Continue Do + End If + End If + End If + Exit Do + Loop While NextPage < 100 And __continue + + If _TempMediaList.Count > 0 Then + For i% = 0 To _TempMediaList.Count - 1 + ThrowAny(Token) + _TempMediaList(i) = GetVideoData(_TempMediaList(i), Responser, MySettings.DownloadUHD.Value) + Next + _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) + End If + Catch ex As Exception + ProcessException(ex, Token, $"data downloading error [{URL}]") + End Try + End Sub + Private Function GetVideoData(ByVal Media As UserMedia, ByVal resp As Response, ByVal DownloadUHD As Boolean) As UserMedia + Try + If Not Media.URL.IsEmptyString Then + Dim r$ = resp.GetResponse(Media.URL) + If Not r.IsEmptyString Then + Dim NewUrl$ = RegexReplace(r, Regex_M3U8) + If Not NewUrl.IsEmptyString Then + Dim appender$ = RegexReplace(NewUrl, Regex_M3U8_Appender) + Dim t$ = If(Media.PictureOption.IsEmptyString, RegexReplace(r, Regex_VideoTitle), Media.PictureOption) + r = resp.GetResponse(NewUrl) If Not r.IsEmptyString Then - Dim ls As List(Of Sizes) = RegexFields(Of Sizes)(r, {M3U8Reparse}, {1, 2}) + Dim ls As List(Of Sizes) = RegexFields(Of Sizes)(r, {Regex_M3U8_Reparse}, {1, 2}) If ls.ListExists And Not DownloadUHD Then ls.RemoveAll(Function(v) Not v.Value.ValueBetween(1, 1080)) If ls.ListExists Then ls.Sort() - m = $"{appender}/{ls(0).Data}" + NewUrl = $"{appender}/{ls(0).Data.StringTrimStart("/")}" ls.Clear() - Dim pID$ = ID - If pID.IsEmptyString Then pID = RegexReplace(r, VideoID) + Dim pID$ = Media.Post.ID + If pID.IsEmptyString Then pID = RegexReplace(r, Regex_VideoID) If pID.IsEmptyString Then pID = "0" - If Not t.IsEmptyString Then t = t.StringRemoveWinForbiddenSymbols(" ") + t = t.StringRemoveWinForbiddenSymbols.StringTrim If t.IsEmptyString Then t = pID Else If t.Length > 100 Then t = Left(t, 100) End If - If Not m.IsEmptyString Then - Return New UserMedia With { - .Type = UTypes.m3u8, - .Post = New UserPost With {.ID = pID}, - .URL = m, + If Not NewUrl.IsEmptyString Then + Return New UserMedia(NewUrl, UTypes.m3u8) With { + .Post = pID, + .URL_BASE = Media.URL, .File = $"{t}.mp4", .PictureOption = appender } @@ -151,18 +213,18 @@ Namespace API.XVIDEOS End If Return Nothing Catch ex As Exception - LogError(ex, $"[XVIDEOS.UserData.GetVideoData({URL})]") + LogError(ex, $"[XVIDEOS.UserData.GetVideoData({Media.URL})]") Return Nothing End Try End Function Friend Function Download(ByVal URL As String, ByVal resp As Response, ByVal DownloadUHD As Boolean, ByVal ID As String) - Dim m As UserMedia = GetVideoData(URL, resp, DownloadUHD, ID) + Dim m As UserMedia = GetVideoData(New UserMedia(URL, UTypes.VideoPre) With {.Post = ID}, resp, DownloadUHD) If Not m.URL.IsEmptyString Then Dim f As SFile = m.File f.Path = MyFile.PathNoSeparator m.State = UStates.Tried Try - f = M3U8.Download(m.URL, m.PictureOption, Settings.FfmpegFile, f) + f = M3U8.Download(m.URL, m.PictureOption, f) m.File = f m.State = UStates.Downloaded Catch ex As Exception @@ -175,7 +237,7 @@ Namespace API.XVIDEOS DownloadContentDefault(Token) End Sub Protected Overrides Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile) As SFile - Return M3U8.Download(Media.URL, Media.PictureOption, Settings.FfmpegFile, DestinationFile) + Return M3U8.Download(Media.URL, Media.PictureOption, DestinationFile) End Function 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 diff --git a/SCrawler/API/Xhamster/Declarations.vb b/SCrawler/API/Xhamster/Declarations.vb new file mode 100644 index 0000000..0d07a8c --- /dev/null +++ b/SCrawler/API/Xhamster/Declarations.vb @@ -0,0 +1,19 @@ +' 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.Globalization +Imports PersonalUtilities.Functions.RegularExpressions +Namespace API.Xhamster + Friend Module Declarations + Friend Const XhamsterSiteKey As String = "AndyProgram_XHamster" + Friend ReadOnly HtmlScript As RParams = RParams.DMS("\