diff --git a/.gitattributes b/.gitattributes index dfe0770..7a84664 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Auto detect text files and perform LF normalization * text=auto + +# Declare files that will always have CRLF line endings on checkout. +*.md text eol=crlf \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4229ef1..170ada8 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -3,11 +3,11 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username -ko_fi: andyprogram +ko_fi: #andyprogram tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry -custom: ['https://blockchair.com/bitcoin/address/BC1Q0NH839FT5TA44DD7L7RLR97XDQAG9V8D6N7XET'] +custom: ['https://github.com/AAndyProgram/SCrawler/blob/main/HowToSupport.md'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d8a0a35..02a349c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -13,11 +13,16 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. **Profile URL**: -2. Do something -3. See error +2. **Post URL**: +3. Do something +4. See error -**Log data** -If the program log contains any data. +
+Log data +
+If the program log contains any data, replace this line with the log data. If the program log does not contain any data, write here about.
+
+
**Expected behavior** A clear and concise description of what you expected to happen. @@ -26,10 +31,13 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Release information (please complete the following information):** - - OS [e.g. Windows 10, Windows 11] - - Architecture [e.g. x86, x64] - - Version [e.g. 2.0.0.0] - - NET.Framework version + - OS: [e.g. Windows 10, Windows 11] + - Architecture: [e.g. x86, x64] + - Version: [e.g. 2023.3.5.0] + - NET.Framework version: + - ffmpeg version (command `ffmpeg -version`): + - yt-dlp version (command `yt-dlp --version`): + - gallery-dl version (command `gallery-dl --version`): **Additional context** Add any other context about the problem here. diff --git a/.gitignore b/.gitignore index 480c100..2798902 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,9 @@ *.user *.userosscache *.sln.docstates -ffmpeg.exe +.obsidian/ ToDo.txt +ToDo.md # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -33,10 +34,7 @@ bld/ [Oo]bj/ [Ll]og/ [Ll]ogs/ -ffmpeg/ -cURL/ Info/ -Hidden/ # Visual Studio 2015/2017 cache/options directory .vs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 762fe83..0d2fbc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,20 +8,21 @@ I welcome requests! Follow these steps to contribute: 1. If you have a code change suggestion, you can post a replacement code block. I also accept pull requests. # How to build from source -1. Delete the ```PersonalUtilities``` project from the solution. -1. Delete the ```PersonalUtilities.Notifications``` project from the solution. -1. Delete the ```cURL``` folder from the solution. -1. Delete the ```ffmpeg.exe``` from the solution. +1. Delete the `PersonalUtilities` project from the solution. +1. Delete the `PersonalUtilities.Notifications` project from the solution. 1. The following libraries must be added to project references with the '**Copy to output folder**' option: - - ```PersonalUtilities.dll``` - - ```PersonalUtilities.Notifications.dll``` - - ```Microsoft.Toolkit.Uwp.Notifications.dll``` - - ```System.ValueTuple.dll``` -1. Import ```PersonalUtilities.Functions``` for the whole project. + - `PersonalUtilities.dll` + - `PersonalUtilities.Notifications.dll` + - `Microsoft.Toolkit.Uwp.Notifications.dll` + - `System.ValueTuple.dll` +1. Import `PersonalUtilities.Functions` for the whole project. **Always use the correct libraries. You must download libraries from the same release date as the code commit date.** # How to request a new site + +**I'm currently not accepting requests to develop new sites.** + 1. Check [issues](https://github.com/AAndyProgram/SCrawler/issues) (open and [closed](https://github.com/AAndyProgram/SCrawler/issues?q=is%3Aissue+is%3Aclosed)) and [discussions](https://github.com/AAndyProgram/SCrawler/discussions) to find your issue. Perhaps I have already answered your request. 1. If you don't find anything, create a new issue with your request. I usually reply as soon as possible (within the next few hours). diff --git a/Changelog.md b/Changelog.md index 7692a25..1de7195 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,47 @@ +# 2023.4.28.0 + +*2023-04-28* + +- Added + - **YouTube** + - **YouTube Music** + - **Mastodon** + - **Pinterest** + - **ThisVid** + - **YouTube downloader (standalone app)** + - Redesigned standalone downloader and update environment + - Added icons to download progress + - Added icons to saved posts downloader + - **Cookies**: new ways to add cookies. You can now export cookies using the browser extension and then import them into SCrawler! + - User creation: ability to extract the user's URL from the buffer and apply parameters if found + - User creation: simplified way to create new users (`Ctrl+Insert` to create a new user with default parameters from clipboard URL) + - Ability to customize the placement of ffmpeg (and other) files + - Ability to customize the command line encoding + - New notification options for standalone downloader + - Reddit: now it can download saved crossposts + - RedGifs: added `UserAgent` option + - Other improvements +- Removed + - User creation: remove the 'Channel' checkbox because it confuses people + - Removed an ability to open SCrawler with `-v` argument + - All ways to create users except URL. You can only properly create a user using the user's URL. +- Plugins + - Added `IDownloadableMedia` interface + - Removed `Channel` option from all functions and enums + - ISiteSettings: added `GetSingleMediaInstance` function + - IPluginContentProvider: added `DownloadSingleObject` function + - IPluginContentProvider: added tokens to `GetMedia` and `Download` functions + - IPluginContentProvider: removed `GetSpecialData` function + - UserMediaTypes: added `Audio` and `AudioPre` enums +- Fixed + - LPSG: attachments not downloading (Issue #114) + - Twitter: saved posts not downloading (Issue #119) + - XVIDEOS: saved posts not downloading + - Deleting labels file + - PornHub: hide unnecessary errors (Issue #116) + - PornHub: photo galleries bug (Issue #115) + - Minor bugs + # 2023.3.5.0 *2023-03-05* diff --git a/FAQ.md b/FAQ.md index f165970..028e401 100644 --- a/FAQ.md +++ b/FAQ.md @@ -14,11 +14,7 @@ Any other questions I will keep in this file. A: https://github.com/AAndyProgram/SCrawler/wiki/Settings#how-to-set-up-cookies ----- - -#### Q: **I can't copy cookies.** - -A: Use the mouse. Don't use ```Ctrl+A```! + ---- @@ -30,21 +26,22 @@ A: This is a GUI program. #### Q: **Will CLI be added in the future?** -A: I do not think so. +A: NO. ---- #### Q: **I want to add "...." site. How to request.** -A: How to request a new site you can read [here](CONTRIBUTING.md#how-to-request-a-new-site) + +**I'm currently not accepting requests to develop new sites.** ---- -#### Q: **Twitter/Instagram download failed.** +#### Q: **Site download failed.** -A: Check your credentials. Both of these sites require cookies. Check your [Twitter tokens](https://github.com/AAndyProgram/SCrawler/wiki/Settings#how-to-find-twitter-tokens) and [Instagram settings](https://github.com/AAndyProgram/SCrawler/wiki/Settings#instagram). If all settings are set, but nothing works, go to [create a new issue](https://github.com/AAndyProgram/SCrawler/issues). Don't forget to attach the LOG. +A: Check your credentials and **[SITES REQUIREMENTS](https://github.com/AAndyProgram/SCrawler/wiki/Settings#sites-requirements)**. If all settings are set, but nothing works, go to [create a new issue](https://github.com/AAndyProgram/SCrawler/issues). Don't forget to attach the LOG. -**[SITES REQUIREMENTS](https://github.com/AAndyProgram/SCrawler/wiki/Settings#sites-requirements)** +**ATTENTION! Issues without URLs will be closed without a response!** ---- @@ -80,7 +77,7 @@ A: The program stored posts IDs in users' folders. For the first time, the progr #### Q: **How to redownload all data** -A: Double-click on the user you want to redownload. In the opened window open folder setting. Delete the files ending with ```_Data.xml``` and ```_Posts.txt```. Download this user again. +A: Double-click on the user you want to redownload. In the opened window open folder setting. Delete the files ending with ```_Data.xml``` and ```_Posts.txt```. Restart SCrawler. Download this user again. ---- @@ -116,4 +113,4 @@ A: I can only [suggest](#q-you-lost-me-your-program-is-too-complicated) you find #### Q: **Can you add a step-by-step guide or video on how to use the program?** -A: **NO**! I will not do it. If you want, you can create a video tutorial and send it to me. Then I add it. All options and what each option does described on the wiki. The wiki also contains a description of all settings and how-to configure them. For complex settings, there is a steep-by-steep guide. Read the [main](README.md) information and [GUIDE](https://github.com/AAndyProgram/SCrawler/wiki/) and you won't have any problems. I have developed a program with an intuitive interface. There is a Settings button, download buttons, a context menu that drops down when a user is clicked, and other controls. Anyone can use it. \ No newline at end of file +A: **NO! NEVER!** The guide fully covers all the functionality of SCrawler! If you don't respect my work, I don't waste my time. If you want, you can create a video tutorial and send it to me. Then I add it. All options and what each option does described on the wiki. The wiki also contains a description of all settings and how-to configure them. For complex settings, there is a steep-by-steep guide. Read the [main](README.md) information and [GUIDE](https://github.com/AAndyProgram/SCrawler/wiki/) and you won't have any problems. I have developed a program with an intuitive interface. There is a Settings button, download buttons, a context menu that drops down when a user is clicked, and other controls. Anyone can use it. \ No newline at end of file diff --git a/HowToSupport.md b/HowToSupport.md index 89ace8e..8cffa11 100644 --- a/HowToSupport.md +++ b/HowToSupport.md @@ -2,7 +2,6 @@ Your support is very valuable to me. Any support is greatly appreciated. Your su You can support the program by: - **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 - :heart: like the program on this site: https://alternativeto.net/software/scrawler/about/ @@ -10,3 +9,4 @@ You can support the program by: - suggest my program as an alternative ([on this site](https://alternativeto.net/software/scrawler/about/)) to any program you have used before I would be very grateful for any support! :blush: + \ No newline at end of file diff --git a/ProgramScreenshots/AppYouTube.png b/ProgramScreenshots/AppYouTube.png new file mode 100644 index 0000000..e8313ae Binary files /dev/null and b/ProgramScreenshots/AppYouTube.png differ diff --git a/ProgramScreenshots/AppYouTubeMusic.png b/ProgramScreenshots/AppYouTubeMusic.png new file mode 100644 index 0000000..32e7886 Binary files /dev/null and b/ProgramScreenshots/AppYouTubeMusic.png differ diff --git a/ProgramScreenshots/AppYouTubePlaylist.png b/ProgramScreenshots/AppYouTubePlaylist.png new file mode 100644 index 0000000..8dea795 Binary files /dev/null and b/ProgramScreenshots/AppYouTubePlaylist.png differ diff --git a/ProgramScreenshots/AppYouTubePlaylistParser.png b/ProgramScreenshots/AppYouTubePlaylistParser.png new file mode 100644 index 0000000..25e419c Binary files /dev/null and b/ProgramScreenshots/AppYouTubePlaylistParser.png differ diff --git a/ProgramScreenshots/AppYouTubeSettings.png b/ProgramScreenshots/AppYouTubeSettings.png new file mode 100644 index 0000000..5144639 Binary files /dev/null and b/ProgramScreenshots/AppYouTubeSettings.png differ diff --git a/ProgramScreenshots/AppYouTubeVideo.png b/ProgramScreenshots/AppYouTubeVideo.png new file mode 100644 index 0000000..212189c Binary files /dev/null and b/ProgramScreenshots/AppYouTubeVideo.png differ diff --git a/ProgramScreenshots/CreateUserClear.png b/ProgramScreenshots/CreateUserClear.png index c1de596..9bac0b3 100644 Binary files a/ProgramScreenshots/CreateUserClear.png and b/ProgramScreenshots/CreateUserClear.png differ diff --git a/ProgramScreenshots/SavedPosts.png b/ProgramScreenshots/SavedPosts.png index f958f62..a86a488 100644 Binary files a/ProgramScreenshots/SavedPosts.png and b/ProgramScreenshots/SavedPosts.png differ diff --git a/ProgramScreenshots/SettingsGlobalBasis.png b/ProgramScreenshots/SettingsGlobalBasis.png index 9cd66bc..5f191d5 100644 Binary files a/ProgramScreenshots/SettingsGlobalBasis.png and b/ProgramScreenshots/SettingsGlobalBasis.png differ diff --git a/ProgramScreenshots/SettingsGlobalBehavior.png b/ProgramScreenshots/SettingsGlobalBehavior.png index 54b6079..4b8be2f 100644 Binary files a/ProgramScreenshots/SettingsGlobalBehavior.png and b/ProgramScreenshots/SettingsGlobalBehavior.png differ diff --git a/ProgramScreenshots/SettingsGlobalChannels.png b/ProgramScreenshots/SettingsGlobalChannels.png index 2199127..cba4b6e 100644 Binary files a/ProgramScreenshots/SettingsGlobalChannels.png and b/ProgramScreenshots/SettingsGlobalChannels.png differ diff --git a/ProgramScreenshots/SettingsGlobalDefaults.png b/ProgramScreenshots/SettingsGlobalDefaults.png index ff0b745..9e8ca07 100644 Binary files a/ProgramScreenshots/SettingsGlobalDefaults.png and b/ProgramScreenshots/SettingsGlobalDefaults.png differ diff --git a/ProgramScreenshots/SettingsGlobalDownloader.png b/ProgramScreenshots/SettingsGlobalDownloader.png new file mode 100644 index 0000000..a5b26ff Binary files /dev/null and b/ProgramScreenshots/SettingsGlobalDownloader.png differ diff --git a/ProgramScreenshots/SettingsGlobalDownloading.png b/ProgramScreenshots/SettingsGlobalDownloading.png index c8717ea..8684650 100644 Binary files a/ProgramScreenshots/SettingsGlobalDownloading.png and b/ProgramScreenshots/SettingsGlobalDownloading.png differ diff --git a/ProgramScreenshots/SettingsGlobalEnvironment.png b/ProgramScreenshots/SettingsGlobalEnvironment.png new file mode 100644 index 0000000..e3fe3c4 Binary files /dev/null and b/ProgramScreenshots/SettingsGlobalEnvironment.png differ diff --git a/ProgramScreenshots/SettingsGlobalFeed.png b/ProgramScreenshots/SettingsGlobalFeed.png index 6ad8d21..e7f8130 100644 Binary files a/ProgramScreenshots/SettingsGlobalFeed.png and b/ProgramScreenshots/SettingsGlobalFeed.png differ diff --git a/ProgramScreenshots/SettingsGlobalNotifications.png b/ProgramScreenshots/SettingsGlobalNotifications.png index e50e7f7..20da7f9 100644 Binary files a/ProgramScreenshots/SettingsGlobalNotifications.png and b/ProgramScreenshots/SettingsGlobalNotifications.png differ diff --git a/ProgramScreenshots/SettingsSiteMastodon.png b/ProgramScreenshots/SettingsSiteMastodon.png new file mode 100644 index 0000000..5c2b069 Binary files /dev/null and b/ProgramScreenshots/SettingsSiteMastodon.png differ diff --git a/ProgramScreenshots/SettingsSiteMastodonAdditional.png b/ProgramScreenshots/SettingsSiteMastodonAdditional.png new file mode 100644 index 0000000..127e5eb Binary files /dev/null and b/ProgramScreenshots/SettingsSiteMastodonAdditional.png differ diff --git a/ProgramScreenshots/SettingsSitePinterest.png b/ProgramScreenshots/SettingsSitePinterest.png new file mode 100644 index 0000000..2fef0cc Binary files /dev/null and b/ProgramScreenshots/SettingsSitePinterest.png differ diff --git a/ProgramScreenshots/SettingsSiteRedGifs.png b/ProgramScreenshots/SettingsSiteRedGifs.png index bc71828..328405c 100644 Binary files a/ProgramScreenshots/SettingsSiteRedGifs.png and b/ProgramScreenshots/SettingsSiteRedGifs.png differ diff --git a/ProgramScreenshots/SettingsSiteThisVid.png b/ProgramScreenshots/SettingsSiteThisVid.png new file mode 100644 index 0000000..cc304cd Binary files /dev/null and b/ProgramScreenshots/SettingsSiteThisVid.png differ diff --git a/ProgramScreenshots/SettingsSiteTwitter.png b/ProgramScreenshots/SettingsSiteTwitter.png index fb193df..a297c34 100644 Binary files a/ProgramScreenshots/SettingsSiteTwitter.png and b/ProgramScreenshots/SettingsSiteTwitter.png differ diff --git a/ProgramScreenshots/SettingsSiteYouTube.png b/ProgramScreenshots/SettingsSiteYouTube.png new file mode 100644 index 0000000..5cd7367 Binary files /dev/null and b/ProgramScreenshots/SettingsSiteYouTube.png differ diff --git a/ProgramsComparison.md b/ProgramsComparison.md index 471d406..78f2fa2 100644 --- a/ProgramsComparison.md +++ b/ProgramsComparison.md @@ -1,6 +1,55 @@ +# yt-dlp + +https://github.com/yt-dlp/yt-dlp/ + +**Great powerful CLI tool that supports hundreds of sites.** + +SCrawler has advanced user management, collections, labels, groups, automatic downloads, a beautiful view, GUI, the ability to add plugins for other sites and much more. Just try it and compare. + +# 4K Video Downloader + +https://www.4kdownload.com/-plbrz/video-downloader + +| Option | SCrawler | 4K Stogram | +| ---- | ---- | ---- | +| User managament | **Advanced** | No | +| Automatic downloads | **Yes** | No | +| Downloading groups | **Yes** | No | +| Labeling users | **Yes** | No | +| Filtering | **Yes** | No | +| Collections | **Yes** | No | +| Specific user folders | **Yes** | No | +| Favorite / Temporary user options | **Yes** | No | +| Plugins support | **Yes** | No | +| Download single video | **Unlimited** | 30 videos per day *(unlimited starts from 12 EUR)* | +| Download videos per channel | **Unlimited** | 5 free *(unlimited starts from 12 EUR)* | +| Download videos per playlist | **Unlimited** | 10 free *(unlimited starts from 12 EUR)* | +| Download video subtitles | **Any for free**: single video, playlist, user/channel, album, etc| Free for single video | +| The number of subtitles you can download for a video | **All of them** | Up to 10 | +| Convert subtitles to additional formats | **Yes** | No | +| Support LRC format | **Yes** | No | +| Select audio codec for audio/video | **Yes** | No | +| Extract and convert additional audio tracks for video | **Yes** | No | +| Simultaneous downloads | **Unlimited** | 1 free, 3 for 12 EUR, 7 for 43 EUR| +| Private YouTube content download | **Free** | Only in paid plans *starts from 12 EUR* | +| **Paid** | **No** | Yes | +| **Free options** | **The program is completely free** | Only **30** videos per day, 5 from a channel, 10 from a playlist | +| Permitted Commercial Use | **Yes** | Starting from 43 EUR | +| Automatic Subscriptions Update | **Free** | Paid (43 EUR) | +| Posts and Captions Export | No | Paid (43 EUR) | +| Advertisements free | **No ADs at all for free** | Paid (43 EUR) | +| Operating Systems | Windows 10+ | Windows 7+, MacOS 10.13+, Ubuntu x64 | +| Select want content type to download | **Yes** | No | +| Instagram support | **Yes** | No | +| Twitter support | **Yes** | No | +| Reddit support | **Yes** | No | +| Other sites support | **Yes** | No | +| Still supported | Yes | Yes | + # 4K Stogram -https://www.4kdownload.com/products/product-stogram + +https://www.4kdownload.com/-ad0p9/stogram | Option | SCrawler | 4K Stogram | | ---- | ---- | ---- | @@ -27,10 +76,10 @@ https://www.4kdownload.com/products/product-stogram | Export and import subscriptions | No | **Yes** | | **Paid** | **No** | Yes | | **Free options** | **The program is completely free** | Only **ONE** profile downloading and only **200 posts** per day | -| Permitted Commercial Use | **Yes** | Starting from 43.56 EUR | -| Automatic Subscriptions Update | **Free** | Paid (43.56 EUR) | -| Posts and Captions Export | No | Paid (43.56 EUR) | -| Advertisements free | **No ADs at all for free** | Paid (14.52) | +| Permitted Commercial Use | **Yes** | Starting from 43 EUR | +| Automatic Subscriptions Update | **Free** | Paid (43 EUR) | +| Posts and Captions Export | No | Paid (43 EUR) | +| Advertisements free | **No ADs at all for free** | Paid (18 EUR) | | Operating Systems | Windows 10+ | Windows 7+, MacOS 10.13+, Ubuntu x64 | | Select want content type to download | **Yes** | No | | Instagram support | Yes | Yes | @@ -68,7 +117,7 @@ https://github.com/RipMeApp/ripme | **Free options** | The program is completely free | The program is completely free, but site limits are not declared | | Operating Systems | Windows 10+ | Windows, MacOS, Linux | | Select want content type to download | Yes | Yes | -| Suported sites | 9 internal and any site using plugins | 86+ sites (declared) | +| Suported sites | 15 internal and any site using plugins | 86+ sites (declared) | | Other sites support | **Yes** | No | | Still supported | **Yes** | **No (last release date May 4, 2021)** | @@ -76,7 +125,6 @@ https://github.com/RipMeApp/ripme https://github.com/mikf/gallery-dl - -**CLI tool**! Configured with JSON files only. Users need to learn complex configuration options, JSON, commands to use that tool. Very difficult to configure. +**CLI tool** SCrawler has advanced user management, collections, labels, groups, automatic downloads, a beautiful view, GUI, the ability to add plugins for other sites and much more. Just try it and compare. \ No newline at end of file diff --git a/README.md b/README.md index 0e5a223..aedabcb 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,39 @@ [![FAQ](https://img.shields.io/badge/FAQ-green)](FAQ.md) [![GUIDE](https://img.shields.io/badge/GUIDE-green)](https://github.com/AAndyProgram/SCrawler/wiki) [![How to support](https://img.shields.io/badge/HowToSupport-green)](HowToSupport.md) +:eu: +:greece: -A program to download photo and video from [any site](#supported-sites) (e.g. Reddit, Twitter, Instagram, TikTok, RedGifs, PornHub, XHamster, XVIDEOS, LPSG). +A program to download photo and video from [any site](#supported-sites) (e.g. YouTube, YouTube Music, Reddit, Twitter, Mastodon, Instagram, TikTok, RedGifs, PornHub, XHamster, XVIDEOS, ThisVid, LPSG, Pinterest). **If you like SCrawler, please like the program on [this site](https://alternativeto.net/software/scrawler/about/) and/or [this](https://www.softpedia.com/get/Internet/Download-Managers/Social-networks-crawler.shtml)** - -Do you like this program? Consider adding to my coffee fund by making a donation to show your support. :blush: - -[![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/andyprogram) - + **Bitcoin**: BC1Q0NH839FT5TA44DD7L7RLR97XDQAG9V8D6N7XET ![Main window](ProgramScreenshots/MainWindow.png) ![Channels window](ProgramScreenshots/Channels.png) +[**YouTube standalone application:**](https://github.com/AAndyProgram/SCrawler/wiki/YouTube%20downloader) + +![YouTube application](ProgramScreenshots/AppYouTube.png) + # What can program do: - Download pictures and videos from users' profiles and subreddits: + - YouTube videos, shorts, users, artists, playlists, music, tracks; - Reddit images, galleries of images, videos, saved posts; - Redgifs videos (https://www.redgifs.com/); - Twitter images and videos, saved (bookmarked) posts; + - Mastodon images and videos, saved (bookmarked) posts; - Instagram images and videos, tagged posts, stories, saved posts; - - TikTok videos ([limited](https://github.com/AAndyProgram/SCrawler/wiki/Settings#tiktok-limits)); + - TikTok videos (*currently broken*; [limited](https://github.com/AAndyProgram/SCrawler/wiki/Settings#tiktok-limits)); + - Pinterest boards, users, saved posts; - Imgur images, galleries and videos; - Gfycat videos; - PornHub images, videos, save (liked) posts; - XHamster images, videos, saved posts; - - XVIDEOS videos; + - XVIDEOS videos, saved posts; + - ThiVid images, videos, saved posts; - [Other](#supported-sites) supported sites - Parse [channel and view data](https://github.com/AAndyProgram/SCrawler/wiki/Channels) - Download [saved Reddit, Twitter and Instagram posts](https://github.com/AAndyProgram/SCrawler/wiki/Home#saved-posts) @@ -52,18 +59,22 @@ Do you like this program? Consider adding to my coffee fund by making a donation - ...and many others... # Supported sites - +- **YouTube** +- **YouTube Music** - **Reddit** - **Twitter** +- **Mastodon** - **Instagram** -- **TikTok** ([limited](https://github.com/AAndyProgram/SCrawler/wiki/Settings#tiktok-limits)) +- TikTok (*currently broken*; [limited](https://github.com/AAndyProgram/SCrawler/wiki/Settings#tiktok-limits)) - RedGifs +- Pinterest - Imgur - Gfycat - LPSG - **PornHub** - **XHamster** - **XVIDEOS** +- **ThisVid** - [Other sites](Plugins.md) **[SITES REQUIREMENTS](https://github.com/AAndyProgram/SCrawler/wiki/Settings#sites-requirements)** @@ -82,7 +93,8 @@ The program parses user posts and compares file names with existing ones to remo ## How to request a new site -Read [here](CONTRIBUTING.md#how-to-request-a-new-site) about + +**I'm currently not accepting requests to develop new sites.** # Requirements @@ -108,12 +120,16 @@ Read [here](CONTRIBUTING.md#how-to-request-a-new-site) about - **[SITES REQUIREMENTS](https://github.com/AAndyProgram/SCrawler/wiki/Settings#sites-requirements)** - [Reddit](https://github.com/AAndyProgram/SCrawler/wiki/Settings#reddit) - [Twitter](https://github.com/AAndyProgram/SCrawler/wiki/Settings#twitter) + - [Mastodon](https://github.com/AAndyProgram/SCrawler/wiki/Settings#Mastodon) - [Instagram](https://github.com/AAndyProgram/SCrawler/wiki/Settings#instagram) - [TikTok](https://github.com/AAndyProgram/SCrawler/wiki/Settings#tiktok) - [RedGifs](https://github.com/AAndyProgram/SCrawler/wiki/Settings#redgifs) + - [YouTube](https://github.com/AAndyProgram/SCrawler/wiki/Settings#YouTube) + - [Pinterest](https://github.com/AAndyProgram/SCrawler/wiki/Settings#Pinterest) - [PornHub](https://github.com/AAndyProgram/SCrawler/wiki/Settings#pornhub) - [XHamster](https://github.com/AAndyProgram/SCrawler/wiki/Settings#xhamster) - [XVIDEOS](https://github.com/AAndyProgram/SCrawler/wiki/Settings#xvideos) + - [ThisVid](https://github.com/AAndyProgram/SCrawler/wiki/Settings#ThisVid) - [LPSG](https://github.com/AAndyProgram/SCrawler/wiki/Settings#lpsg) **Full guide you can find [here](https://github.com/AAndyProgram/SCrawler/wiki)** @@ -142,20 +158,11 @@ The program has an intuitive interface. Just add a user profile and **click the ```Download``` button**. -Read more about adding users and subreddits [here](https://github.com/AAndyProgram/SCrawler/wiki/Users) +Read more about adding users and subreddits [here](https://github.com/AAndyProgram/SCrawler/wiki#Add%20user) ![Add user](ProgramScreenshots/CreateUserClear.png) -# Using program as just video downloader - -Create a shortcut for the program. Open shortcut properties. In the ```Shortcut``` tab, in the ```Target``` field, just add the letter ```v``` at the end across the space. - -Example: ```D:\Programs\SCrawler\SCrawler.exe v``` - -![Separate video downloader](ProgramScreenshots/SeparateVideoDownloader.png) - # Contact me -[![matrix](https://img.shields.io/badge/Matrix-%40andyprogram%3Amatrix.org-informational)](https://matrix.to/#/@andyprogram:matrix.org) - -[![discord](https://img.shields.io/badge/discord-AndyProgram%233804-yellowgreen)](https://discordapp.com/users/1012768226679206009) AndyProgram#3804 \ No newline at end of file +Matrix (Element): https://matrix.to/#/@andyprogram:matrix.org +Discord: AndyProgram#3804 \ No newline at end of file diff --git a/SCrawler.PluginProvider/Interfaces/IDownloadableMedia.vb b/SCrawler.PluginProvider/Interfaces/IDownloadableMedia.vb new file mode 100644 index 0000000..76de874 --- /dev/null +++ b/SCrawler.PluginProvider/Interfaces/IDownloadableMedia.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 +Namespace Plugin + Public Interface IDownloadableMedia : Inherits IUserMedia, IDisposable + Event CheckedChange As EventHandler + Event ThumbnailChanged As EventHandler + Event StateChanged As EventHandler + ReadOnly Property SiteIcon As Drawing.Image + ReadOnly Property Site As String + ReadOnly Property SiteKey As String + Property ThumbnailUrl As String + Property ThumbnailFile As String + Property Title As String + Property Size As Integer + Property Duration As TimeSpan + Property Progress As Object + ReadOnly Property HasError As Boolean + ReadOnly Property Exists As Boolean + Property Checked As Boolean + Property Instance As IPluginContentProvider + Sub Download(ByVal UseCookies As Boolean, ByVal Token As Threading.CancellationToken) + Sub Delete(ByVal RemoveFiles As Boolean) + Sub Load(ByVal File As String) + Sub Save() + Overloads Function ToString() As String + Overloads Function ToString(ByVal ForMediaItem As Boolean) As String + End Interface +End Namespace \ No newline at end of file diff --git a/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb b/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb index 0a38d14..0385313 100644 --- a/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb +++ b/SCrawler.PluginProvider/Interfaces/IPluginContentProvider.vb @@ -8,8 +8,8 @@ ' but WITHOUT ANY WARRANTY Namespace Plugin Public Interface IPluginContentProvider : Inherits IDisposable - Event ProgressChanged(ByVal Count As Integer) - Event TotalCountChanged(ByVal Count As Integer) + Event ProgressChanged(ByVal Value As Integer) + Event ProgressMaximumChanged(ByVal Value As Integer, ByVal Add As Boolean) Property Thrower As IThrower Property LogProvider As ILogProvider Property Settings As ISiteSettings @@ -32,7 +32,8 @@ Namespace Plugin Sub ExchangeOptionsSet(ByVal Obj As Object) Sub XmlFieldsSet(ByVal Fields As List(Of KeyValuePair(Of String, String))) Function XmlFieldsGet() As List(Of KeyValuePair(Of String, String)) - Sub GetMedia() - Sub Download() + Sub GetMedia(ByVal Token As Threading.CancellationToken) + Sub Download(ByVal Token As Threading.CancellationToken) + Sub DownloadSingleObject(ByVal Data As IDownloadableMedia, ByVal Token As Threading.CancellationToken) End Interface End Namespace \ No newline at end of file diff --git a/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb b/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb index 4164e96..0563589 100644 --- a/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb +++ b/SCrawler.PluginProvider/Interfaces/ISiteSettings.vb @@ -12,17 +12,17 @@ Namespace Plugin Enum Download As Integer Main = 0 SavedPosts = 1 - Channel = 2 + SingleObject = 2 End Enum ReadOnly Property Icon As Icon ReadOnly Property Image As Image ReadOnly Property Site As String Property Logger As ILogProvider - Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String + Function GetUserUrl(ByVal User As IPluginContentProvider) 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 GetSingleMediaInstance(ByVal URL As String, ByVal OutputFile As String) As IDownloadableMedia 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))) diff --git a/SCrawler.PluginProvider/My Project/AssemblyInfo.vb b/SCrawler.PluginProvider/My Project/AssemblyInfo.vb index 765443a..3a3c5ee 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/ExchangeOptions.vb b/SCrawler.PluginProvider/Objects/ExchangeOptions.vb index b3667cf..e761d6a 100644 --- a/SCrawler.PluginProvider/Objects/ExchangeOptions.vb +++ b/SCrawler.PluginProvider/Objects/ExchangeOptions.vb @@ -11,15 +11,11 @@ Namespace Plugin Public UserName As String Public SiteName As String Public HostKey As String - Public IsChannel As Boolean Public Exists As Boolean Public Sub New(ByVal Site As String, ByVal Name As String) UserName = Name SiteName = Site - End Sub - Public Sub New(ByVal Site As String, ByVal Name As String, ByVal IsChannel As Boolean) - Me.New(Site, Name) - Me.IsChannel = IsChannel + Exists = Not String.IsNullOrEmpty(Name) And Not String.IsNullOrWhiteSpace(Name) End Sub End Structure End Namespace \ No newline at end of file diff --git a/SCrawler.PluginProvider/Objects/PluginUserMedia.vb b/SCrawler.PluginProvider/Objects/PluginUserMedia.vb index a5d59c8..a16a893 100644 --- a/SCrawler.PluginProvider/Objects/PluginUserMedia.vb +++ b/SCrawler.PluginProvider/Objects/PluginUserMedia.vb @@ -7,12 +7,15 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Namespace Plugin + Public Delegate Sub ProgressChange(ByVal Value As Double?, ByVal Maximum As Double?, ByVal Information As String) Public Enum UserMediaTypes As Integer Undefined = 0 - [Picture] = 1 - [Video] = 2 - [Text] = 3 + Picture = 1 + Video = 2 + Audio = 200 + Text = 4 VideoPre = 10 + AudioPre = 215 GIF = 50 m3u8 = 100 End Enum @@ -24,12 +27,12 @@ Namespace Plugin Missing = 4 End Enum Public Structure PluginUserMedia : Implements IUserMedia - Public Property ContentType As Integer Implements IUserMedia.ContentType + Public Property ContentType As UserMediaTypes 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 DownloadState As UserMediaStates 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 @@ -37,12 +40,12 @@ Namespace Plugin Public Property [Object] As Object Implements IUserMedia.Object End Structure Public Interface IUserMedia - Property ContentType As Integer + Property ContentType As UserMediaTypes Property URL As String Property URL_BASE As String Property MD5 As String Property File As String - Property DownloadState As Integer + Property DownloadState As UserMediaStates Property PostID As String Property PostDate As Date? Property SpecialFolder As String diff --git a/SCrawler.PluginProvider/SCrawler.PluginProvider.vbproj b/SCrawler.PluginProvider/SCrawler.PluginProvider.vbproj index 6b5a7f6..a45fa8b 100644 --- a/SCrawler.PluginProvider/SCrawler.PluginProvider.vbproj +++ b/SCrawler.PluginProvider/SCrawler.PluginProvider.vbproj @@ -102,6 +102,7 @@ + diff --git a/SCrawler.YouTube/.editorconfig b/SCrawler.YouTube/.editorconfig new file mode 100644 index 0000000..18ddd08 --- /dev/null +++ b/SCrawler.YouTube/.editorconfig @@ -0,0 +1,3 @@ +[*.vb] +# Modifier preferences +file_header_template = Copyright (C) 2023 Andy https://github.com/AAndyProgram\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see \ No newline at end of file diff --git a/SCrawler.YouTube/App.config b/SCrawler.YouTube/App.config new file mode 100644 index 0000000..5534e28 --- /dev/null +++ b/SCrawler.YouTube/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SCrawler.YouTube/Attributes/GridVisibleAttribute.vb b/SCrawler.YouTube/Attributes/GridVisibleAttribute.vb new file mode 100644 index 0000000..5aa85fd --- /dev/null +++ b/SCrawler.YouTube/Attributes/GridVisibleAttribute.vb @@ -0,0 +1,30 @@ +' 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.YouTube.Attributes + + Public Class GridVisibleAttribute : Inherits Attribute + Private ReadOnly NonAppMode As Boolean = True + Public Sub New() + End Sub + Public Sub New(ByVal NonAppMode As Boolean) + Me.NonAppMode = NonAppMode + End Sub + Public Overrides Function Equals(ByVal Obj As Object) As Boolean + If Not Obj Is Nothing AndAlso TypeOf Obj Is GridVisibleAttribute Then + If NonAppMode Then + Return DirectCast(Obj, GridVisibleAttribute).NonAppMode + Else + Return True + End If + Else + Return False + End If + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Base/Structures.vb b/SCrawler.YouTube/Base/Structures.vb new file mode 100644 index 0000000..58b168b --- /dev/null +++ b/SCrawler.YouTube/Base/Structures.vb @@ -0,0 +1,81 @@ +' 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.YouTube.Base + Public Structure Thumbnail : Implements IIndexable, IComparable(Of Thumbnail) + Public ID As String + Public Width As Integer + Public Height As Integer + Public URL As String + Public Property Index As Integer Implements IIndexable.Index + Private Function SetIndex(ByVal Obj As Object, ByVal Index As Integer) As Object Implements IIndexable.SetIndex + Dim t As Thumbnail = Obj + t.Index = Index + Return t + End Function + Private Function CompareTo(ByVal Other As Thumbnail) As Integer Implements IComparable(Of Thumbnail).CompareTo + Return Width.CompareTo(Other.Width) * -1 + End Function + End Structure + Public Structure Subtitles : Implements IIndexable, IComparable(Of Subtitles) + Public ID As String + Public Name As String + Public Formats As String + Public ReadOnly Property FullID As String + Get + Return IIf(ID = "en", "en.*", ID) + End Get + End Property + Public Property Index As Integer Implements IIndexable.Index + Private Function SetIndex(ByVal Obj As Object, ByVal Index As Integer) As Object Implements IIndexable.SetIndex + Dim s As Subtitles = Obj + s.Index = Index + Return s + End Function + Private Function CompareTo(ByVal Other As Subtitles) As Integer Implements IComparable(Of Subtitles).CompareTo + Return Name.CompareTo(Other.Name) + End Function + End Structure + Public Enum YouTubeMediaType As Integer + Undefined = 0 + [Single] = 1 + Channel = 2 + PlayList = 3 + End Enum + Public Structure MediaObject : Implements IIndexable, IComparable(Of MediaObject) + Public Type As Plugin.UserMediaTypes + Public ID As String + Public Extension As String + Public Width As Integer + Public Height As Integer + Public FPS As Integer + Public Bitrate As Integer + ''' Kb + Public Size As Double + Public Codec As String + Public Info As String + Public URL As String + Public Property Index As Integer Implements IIndexable.Index + Private Function SetIndex(ByVal Obj As Object, ByVal Index As Integer) As Object Implements IIndexable.SetIndex + Dim m As MediaObject = Obj + m.Index = Index + Return m + End Function + Private Function CompareTo(ByVal Other As MediaObject) As Integer Implements IComparable(Of MediaObject).CompareTo + If Type = Other.Type Then + If Width.CompareTo(Other.Width) = 0 Then + Return Size.CompareTo(Other.Size) * -1 + Else + Return Width.CompareTo(Other.Width) * -1 + End If + Else + Return CInt(Type).CompareTo(CInt(Other.Type)) + End If + End Function + End Structure +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Base/TableControlsProcessor.vb b/SCrawler.YouTube/Base/TableControlsProcessor.vb new file mode 100644 index 0000000..a24f533 --- /dev/null +++ b/SCrawler.YouTube/Base/TableControlsProcessor.vb @@ -0,0 +1,38 @@ +' 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.YouTube.Controls + Friend Class TableControlsProcessor + Private ReadOnly Property TP_CONTROLS As TableLayoutPanel + Friend Sub New(ByRef TP As TableLayoutPanel) + TP_CONTROLS = TP + End Sub + Private _LatestSelected As Integer = -1 + Friend Sub MediaItem_Click(ByVal Sender As Object, ByVal e As EventArgs) + Try + _LatestSelected = TP_CONTROLS.GetPositionFromControl(Sender).Row + DirectCast(Sender, Control).Focus() + Catch ex As Exception + _LatestSelected = -1 + End Try + End Sub + Friend Sub MediaItem_KeyDown(ByVal Sender As Object, ByVal e As KeyEventArgs) + Try + If e.KeyCode = Keys.Down Or e.KeyCode = Keys.Up Then + Dim newPosition% = _LatestSelected + IIf(e.KeyCode = Keys.Down, 1, -1) + If newPosition < 0 Then newPosition = 0 + If newPosition <> _LatestSelected Then + Dim cnt As DownloadObjects.STDownloader.MediaItem = TP_CONTROLS.GetControlFromPosition(0, newPosition) + If Not cnt Is Nothing Then cnt.PerformClick() + End If + End If + Catch + End Try + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Base/YouTubeFunctions.vb b/SCrawler.YouTube/Base/YouTubeFunctions.vb new file mode 100644 index 0000000..b686a33 --- /dev/null +++ b/SCrawler.YouTube/Base/YouTubeFunctions.vb @@ -0,0 +1,165 @@ +' 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.Tools +Imports PersonalUtilities.Forms.Toolbars +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports SCrawler.API.YouTube.Objects +Namespace API.YouTube.Base + Public NotInheritable Class YouTubeFunctions + Public Const YouTubeCachePathRoot As String = "_CacheYouTube\" + Public Const UserChannelOption As String = "channel" + Public Const TrueUrlPattern As String = "https?://[^/]*?youtube.com/[^\?/&]+((\??[^\?/&]+|/[^\?/&]+))" + '2 - type; 5 - id + Public Const UrlTypePattern As String = "(?<=https?://[^/]*?youtube.com/)((@|[^\?/&]+))([/\?]{0,1}(list=|v=|)([^\?/&]*))(?=(\S+|\Z|))" + Private Sub New() + End Sub + Public Shared Function IsMyUrl(ByVal URL As String) As Boolean + Return Not Info_GetUrlType(URL) = YouTubeMediaType.Undefined + End Function + Public Shared Function Info_GetUrlType(ByVal URL As String, Optional ByRef IsMusic As Boolean = False, + Optional ByRef IsChannelUser As Boolean = False, Optional ByRef Id As String = Nothing) As YouTubeMediaType + If Not URL.IsEmptyString Then + IsMusic = URL.Contains("music.youtube.com") + IsChannelUser = False + Dim data As List(Of String) = RegexReplace(URL, RParams.DMS(UrlTypePattern, 0, RegexReturn.ListByMatch, EDP.ReturnValue)) + If data.ListExists Then + If data.Count >= 6 Then Id = data(5) + If data.Count >= 3 And Not data(2).IsEmptyString Then + Select Case data(2).ToLower + Case "watch" : Return YouTubeMediaType.Single + Case "playlist" : Return YouTubeMediaType.PlayList + Case UserChannelOption, "@" : IsChannelUser = data(2).ToLower = UserChannelOption : Return YouTubeMediaType.Channel + End Select + End If + End If + End If + Return YouTubeMediaType.Undefined + End Function + ''' '--no-cookies-from-browser --cookies CookiesFile' + Public Shared Function GetCookiesCommand(ByVal UseCookies As Boolean, ByVal CookiesFile As SFile) As String + If UseCookies And CookiesFile.Exists Then + Return $"--no-cookies-from-browser --cookies ""{CookiesFile}""" + Else + Return String.Empty + End If + End Function + ''' Data with upload date 'more than or equal to' date will be downloaded + ''' Data with upload date 'less than or equal to' date will be downloaded + ''' + ''' + ''' + Public Shared Function Parse(ByVal URL As String, Optional ByVal UseCookies As Boolean? = Nothing, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing, + Optional ByVal GetDefault As Boolean? = Nothing, Optional ByVal GetShorts As Boolean? = Nothing, + Optional ByVal DateAfter As Date? = Nothing, Optional ByVal DateBefore As Date? = Nothing) As IYouTubeMediaContainer + If URL.IsEmptyString Then Throw New ArgumentNullException("URL", "URL cannot be null") + If Not MyYouTubeSettings.YTDLP.Value.Exists Then Throw New IO.FileNotFoundException("Path to 'yt-dlp.exe' not set or program not found at destination", MyYouTubeSettings.YTDLP.Value.ToString) + Dim urlOrig$ = URL + URL = RegexReplace(URL, TrueUrlRegEx) + If URL.IsEmptyString Then Throw New ArgumentNullException("URL", $"Can't get true URL from [{urlOrig}]") + Dim isMusic As Boolean = False + Dim objType As YouTubeMediaType = Info_GetUrlType(URL, isMusic) + If Not objType = YouTubeMediaType.Undefined Then + Dim __GetDefault As Boolean = If(GetDefault, True) + Dim __GetShorts As Boolean = If(GetShorts, True) + If isMusic Then __GetShorts = False + Dim container As IYouTubeMediaContainer + Dim pattern$ = "%(channel_id)s_%(id)s_%(playlist_index)s" + + Select Case objType + Case YouTubeMediaType.Single + __GetShorts = False + If isMusic Then container = New Track Else container = New Video + Case YouTubeMediaType.PlayList : container = New PlayList : pattern = "%(playlist_index)s_%(id)s" : __GetShorts = False + Case YouTubeMediaType.Channel + container = New Channel + If isMusic Then pattern = "%(playlist_id)s/%(channel_id)s_%(id)s_%(playlist_index)s" + Case Else : Throw New InvalidOperationException($"Type '{objType}' is not supported by YouTubeDownloader") + End Select + + If UseCookies.HasValue Then container.UseCookies = UseCookies.Value + Dim result As Boolean = False + Dim cookiesExists As Boolean = YouTubeCookieNetscapeFile.Exists + Dim _CachePathDefault As SFile = MyCache.NewPath(, EDP.ReturnValue) + If _CachePathDefault.IsEmptyString Then _CachePathDefault = $"{YouTubeCachePathRoot}{SFile.GetDirectories(YouTubeCachePathRoot,,, EDP.ReturnValue).Count + 1}" + _CachePathDefault.Exists(SFO.Path, True, EDP.ThrowException) + pattern = $"{_CachePathDefault.PathWithSeparator}{pattern}" + + Dim withCookieRequested As Boolean = False + Dim useCookiesForce As Boolean = UseCookies.HasValue AndAlso UseCookies.Value AndAlso cookiesExists + If UseCookies.HasValue AndAlso UseCookies.Value Then + withCookieRequested = True + result = Parse_Internal(URL, pattern, _CachePathDefault, True, YouTubeCookieNetscapeFile, DateAfter, DateBefore, __GetDefault, __GetShorts) + End If + If Not result And Not withCookieRequested Then + If Not UseCookies.HasValue OrElse Not UseCookies.Value Then result = Parse_Internal(URL, pattern, _CachePathDefault, False, YouTubeCookieNetscapeFile, DateAfter, DateBefore, __GetDefault, __GetShorts) + If Not result And Not UseCookies.HasValue And cookiesExists Then result = Parse_Internal(URL, pattern, _CachePathDefault, True, YouTubeCookieNetscapeFile, DateAfter, DateBefore, __GetDefault, __GetShorts) + End If + + If result Then + container.Parse(Nothing, _CachePathDefault, isMusic, Token, Progress) + If Not container.HasError Then container.URL = URL : Return container + End If + container.Dispose() + End If + Return Nothing + End Function + Private Shared Function Parse_Internal(ByVal URL As String, ByVal OutputPattern As String, ByVal OutputPath As SFile, + ByVal UseCookies As Boolean, ByVal CookiesFile As SFile, + ByVal DateAfter As Date?, ByVal DateBefore As Date?, + ByVal GetDefault As Boolean, ByVal GetShorts As Boolean) As Boolean + Try + Dim command$ = "yt-dlp --write-info-json --skip-download" + command.StringAppend(GetCookiesCommand(UseCookies, CookiesFile), " ") + If DateAfter.HasValue Then command.StringAppend($"--dateafter {DateAfter.Value:yyyyMMdd}", " ") + If DateBefore.HasValue Then command.StringAppend($"--datebefore {DateBefore.Value:yyyyMMdd}", " ") + command.StringAppend("{0}" & $" -o ""{OutputPattern}""", " ") +#If DEBUG Then + Debug.WriteLine(String.Format(command, URL)) +#End If + Using batch As New BatchExecutor(True) + With batch + .CommandPermanent = BatchExecutor.GetDirectoryCommand(MyYouTubeSettings.YTDLP.Value) + If GetDefault Then .Execute(String.Format(command, URL)) + If GetShorts Then .Execute(String.Format(command, $"{URL.StringTrimEnd("/")}/shorts")) + End With + End Using + Return SFile.GetFiles(OutputPath,, IO.SearchOption.AllDirectories, EDP.ReturnValue).Count > 0 + Catch ex As Exception + Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, + $"[API.YouTube.Base.YouTubeFunctions.Parse_Internal({URL}, {UseCookies})]", False) + End Try + End Function + Friend Shared Function CreateContainer(ByVal f As SFile) As IYouTubeMediaContainer + Dim c As IYouTubeMediaContainer = Nothing + If f.Exists(SFO.File, False) Then + Using x As New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True} + x.LoadData() + If x.Value(YouTubeMediaContainerBase.Name_SiteKey) = YouTubeSiteKey Then + Select Case x.Value(YouTubeMediaContainerBase.Name_ObjectType).FromXML(Of Integer)(YouTubeMediaType.Undefined) + Case YouTubeMediaType.Channel : c = New Channel + Case YouTubeMediaType.PlayList : c = New PlayList + Case YouTubeMediaType.Single + If x.Value(YouTubeMediaContainerBase.Name_IsMusic).FromXML(Of Boolean)(False) Then + c = New Track + Else + c = New Video + End If + Case Else : Throw New ArgumentException($"Object type '{x.Value(YouTubeMediaContainerBase.Name_ObjectType)}' is not identified", + "ObjectType") With {.HelpLink = NameOf(CreateContainer)} + End Select + End If + End Using + If Not c Is Nothing Then c.Load(f) + End If + Return c + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Base/YouTubeSettings.vb b/SCrawler.YouTube/Base/YouTubeSettings.vb new file mode 100644 index 0000000..58b604b --- /dev/null +++ b/SCrawler.YouTube/Base/YouTubeSettings.vb @@ -0,0 +1,337 @@ +' 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 System.Drawing.Design +Imports System.ComponentModel +Imports SCrawler.API.YouTube.Attributes +Imports SCrawler.DownloadObjects.STDownloader +Imports PersonalUtilities.Forms +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Functions.XML.Objects +Imports PersonalUtilities.Functions.XML.Attributes.Specialized +Imports PersonalUtilities.Tools +Imports PersonalUtilities.Tools.Grid.Base +Imports PersonalUtilities.Tools.Grid.Attributes +Imports PersonalUtilities.Tools.Grid.Collections +Imports PersonalUtilities.Tools.Grid.Specialized +Imports PersonalUtilities.Tools.Web.Cookies +Namespace API.YouTube.Base + + Public Class YouTubeSettings : Implements IXMLValuesContainer, IGridValuesContainer, IDownloaderSettings +#Region "Events" + Private Event OnBeginUpdate As EventHandler Implements IXMLValuesContainer.OnBeginUpdate + Private Event OnEndUpdate As EventHandler Implements IXMLValuesContainer.OnEndUpdate +#End Region +#Region "Declarations" + Private ReadOnly Property XML As XmlFile Implements IXMLValuesContainer.XML + Friend ReadOnly Property DesignXml As XmlFile + Private Property Mode As GridUpdateModes = GridUpdateModes.OnConfirm Implements IGridValuesContainer.Mode + Friend ReadOnly Property PlaylistFormSplitterDistance As XMLValue(Of Integer) +#Region "Environment" + + Public ReadOnly Property YTDLP As XMLValue(Of SFile) + + Public ReadOnly Property FFMPEG As XMLValue(Of SFile) + + Public ReadOnly Property Cookies As CookieKeeper + Private Function ShouldSerializeCookies() As Boolean + Return Cookies.Count > 0 + End Function + Private Sub ResetCookies() + Cookies.Clear() + End Sub + Private Class CookieListForm2 : Inherits CookieListForm + Public Sub New() + ShowGrid = False + End Sub + End Class + + Public ReadOnly Property OutputPath As XMLValue(Of SFile) + + Public ReadOnly Property OutputPathAutoChange As XMLValue(Of Boolean) + + Public ReadOnly Property OnItemDoubleClick As XMLValue(Of DoubleClickBehavior) + Private ReadOnly Property IDownloaderSettings_OnItemDoubleClick As DoubleClickBehavior Implements IDownloaderSettings.OnItemDoubleClick + Get + Return OnItemDoubleClick + End Get + End Property + + Public ReadOnly Property OpenFolderInOtherProgram As XMLValueUse(Of String) + + Private Property IDownloaderSettings_OpenFolderInOtherProgram As Boolean Implements IDownloaderSettings.OpenFolderInOtherProgram + Get + Return OpenFolderInOtherProgram.Use + End Get + Set(ByVal use As Boolean) + OpenFolderInOtherProgram.Use = use + End Set + End Property + + Private Property IDownloaderSettings_OpenFolderInOtherProgram_Command As String Implements IDownloaderSettings.OpenFolderInOtherProgram_Command + Get + Return OpenFolderInOtherProgram + End Get + Set(ByVal command As String) + OpenFolderInOtherProgram.Value = command + End Set + End Property +#End Region +#Region "Defaults" + + Public ReadOnly Property DefaultUseCookies As XMLValue(Of Boolean) + + Public ReadOnly Property ItemsListLimit As XMLValue(Of Integer) + + Public ReadOnly Property RemoveDownloadedAutomatically As XMLValue(Of Boolean) + Private ReadOnly Property IDownloaderSettings_RemoveDownloadedAutomatically As Boolean Implements IDownloaderSettings.RemoveDownloadedAutomatically + Get + Return RemoveDownloadedAutomatically + End Get + End Property + + Public ReadOnly Property DownloadAutomatically As XMLValue(Of Boolean) + Private ReadOnly Property IDownloaderSettings_DownloadAutomatically As Boolean Implements IDownloaderSettings.DownloadAutomatically + Get + Return DownloadAutomatically + End Get + End Property + + Public ReadOnly Property MaxJobsCount As XMLValue(Of Integer) + Private ReadOnly Property IDownloaderSettings_MaxJobsCount As Integer Implements IDownloaderSettings.MaxJobsCount + Get + Return MaxJobsCount + End Get + End Property + + Public ReadOnly Property ShowNotifications As XMLValue(Of Boolean) + Private ReadOnly Property IDownloaderSettings_ShowNotifications As Boolean Implements IDownloaderSettings.ShowNotifications + Get + Return ShowNotifications + End Get + End Property + + Public ReadOnly Property ShowNotificationsEveryDownload As XMLValue(Of Boolean) + Private ReadOnly Property IDownloaderSettings_ShowNotificationsEveryDownload As Boolean Implements IDownloaderSettings.ShowNotificationsEveryDownload + Get + Return ShowNotifications And ShowNotificationsEveryDownload + End Get + End Property + Private Sub ShowNotificationsEveryDownload_TempValueChanged(ByVal Sender As Object, ByVal e As EventArgs) + If ShowNotificationsEveryDownload.ValueTemp Then ShowNotifications.ValueTemp = True + End Sub + + Public ReadOnly Property CloseToTray As XMLValue(Of Boolean) + + Public ReadOnly Property ExitConfirm As XMLValue(Of Boolean) +#End Region +#Region "Defaults Video" + + Public ReadOnly Property DefaultVideoFormat As XMLValue(Of String) + Private Function AvailableVideoFormats_Impl() As String() + Return AvailableVideoFormats + End Function + + Public ReadOnly Property DefaultVideoDefinition As XMLValue(Of Integer) +#End Region +#Region "Defaults Audio" + + Public ReadOnly Property DefaultAudioCodec As XMLValue(Of String) + Private Function AvailableAudioFormats_Impl() As String() + Return AvailableAudioFormats + End Function + + Public ReadOnly Property DefaultAudioCodecMusic As XMLValue(Of String) + + Public ReadOnly Property DefaultAudioCodecAddit As XMLValuesCollection(Of String) +#End Region +#Region "Defaults Subtitles" + + Public ReadOnly Property DefaultSubtitles As XMLValuesCollection(Of String) + + Private Property DefaultSubtitles_Impl As String + Get + If DefaultSubtitles.ValueTemp.Count > 0 Then + Return DefaultSubtitles.ValueTemp.ListToString(",") + Else + Return String.Empty + End If + End Get + Set(ByVal s As String) + If s.IsEmptyString Then + DefaultSubtitles.ValueTemp = Nothing + Else + DefaultSubtitles.ValueTemp = ListAddList(Nothing, s.Split(","), LAP.NotContainsOnly, + CType(Function(Input$) Input.StringTrim, Func(Of Object, Object))) + End If + End Set + End Property + Private Function ShouldSerializeDefaultSubtitles_Impl() As Boolean + Return DirectCast(DefaultSubtitles, IGridValue).ShouldSerializeValue + End Function + Private Sub ResetDefaultSubtitles_Impl() + DirectCast(DefaultSubtitles, IGridValue).ResetValue() + End Sub + + Public ReadOnly Property DefaultSubtitlesFormat As XMLValue(Of String) + Private Function AvailableSubtitlesFormats_Impl() As String() + Return AvailableSubtitlesFormats + End Function + + Public ReadOnly Property DefaultSubtitlesFormatAddit As XMLValuesCollection(Of String) +#End Region +#End Region +#Region "Initializer" + Public Sub New() + XML = New XmlFile(YouTubeSettingsFile,, False) With {.AutoUpdateFile = True} + XML.LoadData(EDP.None) + DesignXml = New XmlFile("Settings\DesignDownloader.xml", Protector.Modes.All, False) + DesignXml.LoadData(EDP.None) + InitializeXMLValueProperties(Me) + AddHandler ShowNotificationsEveryDownload.TempValueChanged, AddressOf ShowNotificationsEveryDownload_TempValueChanged + Cookies = New CookieKeeper + Grid.Abstract.DesignerXmlSource.Add(New Grid.Abstract.DesignerXmlData(GetType(CookieListForm2), DesignXml, "CookiesListForm")) + If YouTubeCookieNetscapeFile.Exists Then Cookies.AddRange(CookieKeeper.ParseNetscapeText(YouTubeCookieNetscapeFile.GetText(EDP.ReturnValue), EDP.None),, EDP.None) + If Not YTDLP.Value.Exists Then YTDLP.Value = ProgramPath("yt-dlp.exe") + If Not FFMPEG.Value.Exists Then FFMPEG.Value = ProgramPath("ffmpeg.exe") + If Not OutputPath.Value.Exists(SFO.Path, False) Then OutputPath.Value = YouTubeDownloadPathDefault + If XML.ChangesDetected Then XML.UpdateData() + End Sub + Private Function ProgramPath(ByVal Program As String) As SFile + If Program.CSFile.Exists Then + Return Program.CSFile + ElseIf $"Environment\{Program}".CSFile.Exists Then + Return $"Environment\{Program}" + Else + Return SystemEnvironment.FindFileInPaths(Program).ListIfNothing.FirstOrDefault + End If + End Function +#End Region +#Region "Edit, Update" + Protected Overridable Sub BeginUpdate() Implements IXMLValuesContainer.BeginUpdate, IGridValuesContainer.BeginUpdate + XML.BeginUpdate() + End Sub + Protected Overridable Sub EndUpdate() Implements IXMLValuesContainer.EndUpdate, IGridValuesContainer.EndUpdate + XML.EndUpdate() + If XML.ChangesDetected Then XML.UpdateData() + End Sub + Protected Overridable Sub Apply() Implements IGridValuesContainer.Apply + XMLValuesApply(Me) + ApplyCookies() + End Sub + Protected Sub ApplyCookies() + If Cookies.Count > 0 Then Cookies.SaveNetscapeFile(YouTubeCookieNetscapeFile) Else YouTubeCookieNetscapeFile.Delete(,, EDP.None) + End Sub + Private Sub BeginEdit() Implements IGridValuesContainer.BeginEdit + XMLValuesBeginEdit(Me) + End Sub + Protected Overridable Sub EndEdit() Implements IGridValuesContainer.EndEdit + XMLValuesEndEdit(Me) + Cookies.Clear() + If YouTubeCookieNetscapeFile.Exists Then Cookies.AddRange(CookieKeeper.ParseNetscapeText(YouTubeCookieNetscapeFile.GetText(EDP.ReturnValue), EDP.None),, EDP.None) + End Sub + Public Sub ShowForm(ByVal AppMode As Boolean) + Using f As New SimpleGridForm(Me) With { + .GridShowToolbar = False, + .InitialOkValue = True, + .ShowIcon = True, + .Icon = My.Resources.SiteYouTube.YouTubeIcon_32, + .Text = "YouTube Settings", + .DesignXML = DesignXml, + .DesignXMLNodeName = "YouTubeSettingsForm" + } + f.GridBrowsableAttributes = New AttributeCollection(New BrowsableAttribute(True), New GridVisibleAttribute(Not AppMode)) + f.ShowDialog() + End Using + End Sub +#End Region +#Region "Close" + Friend Sub Close() + DesignXml.Dispose() + XML.Dispose() + Cookies.Dispose() + End Sub +#End Region +#Region "Grid Support" + Private Class ValueCollectionConverter : Inherits TypeConverter + Public Overrides Function ConvertTo(ByVal Context As ITypeDescriptorContext, ByVal Culture As CultureInfo, ByVal Value As Object, ByVal DestinationType As Type) As Object + If TypeOf Value Is IEnumerable Then + Return DirectCast(Value, IEnumerable).ToObjectsList(Of String).ListToString + Else + Return String.Empty + End If + End Function + End Class + Private Class ValueCollectionEditor : Inherits GridStructureCollectionEditor + Public Overrides Function EditValue(ByVal Context As ITypeDescriptorContext, ByVal Provider As IServiceProvider, ByVal Value As Object) As Object + Dim eObj As IEnumerable(Of String) = Nothing + Select Case Context.PropertyDescriptor.Name + Case NameOf(DefaultSubtitlesFormatAddit) : eObj = AvailableSubtitlesFormats + Case NameOf(DefaultAudioCodecAddit) : eObj = AvailableAudioFormats + End Select + Using f As New SimpleListForm(Of String)(eObj) With { + .Mode = SimpleListFormModes.CheckedItems, + .DesignXML = MyYouTubeSettings.DesignXml, + .DesignXMLNodeName = "YouTubeSettingsFormList", + .FormText = DirectCast(Context.PropertyDescriptor.Attributes.Cast(Of Attribute).First(Function(a) a.GetType Is GetType(DisplayNameAttribute)), DisplayNameAttribute).DisplayName, + .Icon = My.Resources.SiteYouTube.YouTubeIcon_32 + } + f.DataSelected.ListAddList(Value) + If f.ShowDialog() = DialogResult.OK Then + eObj = f.DataResult.ToList + With DirectCast(Value, List(Of String)) : .Clear() : .ListAddList(eObj) : End With + End If + End Using + Return Value + End Function + End Class +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Content/Icons/YouTubeIcon_32.ico b/SCrawler.YouTube/Content/Icons/YouTubeIcon_32.ico new file mode 100644 index 0000000..4e4c076 Binary files /dev/null and b/SCrawler.YouTube/Content/Icons/YouTubeIcon_32.ico differ diff --git a/SCrawler.YouTube/Content/Icons/YouTubeMusicIcon_32.ico b/SCrawler.YouTube/Content/Icons/YouTubeMusicIcon_32.ico new file mode 100644 index 0000000..e341bc9 Binary files /dev/null and b/SCrawler.YouTube/Content/Icons/YouTubeMusicIcon_32.ico differ diff --git a/SCrawler.YouTube/Content/Pictures/ArrowDownPic_Blue_24.png b/SCrawler.YouTube/Content/Pictures/ArrowDownPic_Blue_24.png new file mode 100644 index 0000000..e00246f Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/ArrowDownPic_Blue_24.png differ diff --git a/SCrawler.YouTube/Content/Pictures/AudioMusic_32.png b/SCrawler.YouTube/Content/Pictures/AudioMusic_32.png new file mode 100644 index 0000000..de3cfcc Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/AudioMusic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/ClockPic_16.png b/SCrawler.YouTube/Content/Pictures/ClockPic_16.png new file mode 100644 index 0000000..03ea118 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/ClockPic_16.png differ diff --git a/SCrawler.YouTube/Content/Pictures/HeartPic_32.png b/SCrawler.YouTube/Content/Pictures/HeartPic_32.png new file mode 100644 index 0000000..c76b691 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/HeartPic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/ImagePic_32.png b/SCrawler.YouTube/Content/Pictures/ImagePic_32.png new file mode 100644 index 0000000..dd88931 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/ImagePic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/InfoPic_32.png b/SCrawler.YouTube/Content/Pictures/InfoPic_32.png new file mode 100644 index 0000000..872abf3 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/InfoPic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/LinkPic_32.png b/SCrawler.YouTube/Content/Pictures/LinkPic_32.png new file mode 100644 index 0000000..6ea0d79 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/LinkPic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/RulerPic_32.png b/SCrawler.YouTube/Content/Pictures/RulerPic_32.png new file mode 100644 index 0000000..8e1b534 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/RulerPic_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/SettingsPic_16.bmp b/SCrawler.YouTube/Content/Pictures/SettingsPic_16.bmp new file mode 100644 index 0000000..ab2e01d Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/SettingsPic_16.bmp differ diff --git a/SCrawler.YouTube/Content/Pictures/VideoCamera_32.png b/SCrawler.YouTube/Content/Pictures/VideoCamera_32.png new file mode 100644 index 0000000..ac7ada3 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/VideoCamera_32.png differ diff --git a/SCrawler.YouTube/Content/Pictures/YouTubeMusicPic_96.png b/SCrawler.YouTube/Content/Pictures/YouTubeMusicPic_96.png new file mode 100644 index 0000000..a0e3ae2 Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/YouTubeMusicPic_96.png differ diff --git a/SCrawler.YouTube/Content/Pictures/YouTubePic_96.png b/SCrawler.YouTube/Content/Pictures/YouTubePic_96.png new file mode 100644 index 0000000..358e8da Binary files /dev/null and b/SCrawler.YouTube/Content/Pictures/YouTubePic_96.png differ diff --git a/SCrawler.YouTube/Controls/MusicPlaylistsForm.Designer.vb b/SCrawler.YouTube/Controls/MusicPlaylistsForm.Designer.vb new file mode 100644 index 0000000..5589941 --- /dev/null +++ b/SCrawler.YouTube/Controls/MusicPlaylistsForm.Designer.vb @@ -0,0 +1,469 @@ +' 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.YouTube.Controls + + Partial Friend Class MusicPlaylistsForm : 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() + Me.components = New System.ComponentModel.Container() + Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel + Dim TP_BUTTONS As System.Windows.Forms.TableLayoutPanel + Dim TP_PLS As System.Windows.Forms.TableLayoutPanel + Dim TP_PLS_BUTTONS As System.Windows.Forms.TableLayoutPanel + Dim TP_PLS_ITEMS As System.Windows.Forms.TableLayoutPanel + Dim TP_SETTINGS As System.Windows.Forms.TableLayoutPanel + Dim TP_FORMATS As System.Windows.Forms.TableLayoutPanel + Dim ActionButton1 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MusicPlaylistsForm)) + Dim ActionButton2 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton3 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim LBL_FORMAT As System.Windows.Forms.Label + Dim TP_LYRICS As System.Windows.Forms.TableLayoutPanel + Dim ActionButton4 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton5 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton6 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton7 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton8 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim TT_MAIN As System.Windows.Forms.ToolTip + Me.BTT_DOWN = New System.Windows.Forms.Button() + Me.BTT_CANCEL = New System.Windows.Forms.Button() + Me.SPLITTER_MAIN = New System.Windows.Forms.SplitContainer() + Me.LIST_PLAYLISTS = New System.Windows.Forms.CheckedListBox() + Me.BTT_PLS_ALL = New System.Windows.Forms.Button() + Me.BTT_PLS_NONE = New System.Windows.Forms.Button() + Me.LIST_ITEMS = New System.Windows.Forms.CheckedListBox() + Me.TXT_FORMATS_ADDIT = New PersonalUtilities.Forms.Controls.TextBoxExtended() + Me.CMB_FORMATS = New System.Windows.Forms.ComboBox() + Me.TXT_SUBS = New PersonalUtilities.Forms.Controls.TextBoxExtended() + Me.CH_DOWN_LYRICS = New System.Windows.Forms.CheckBox() + Me.TXT_OUTPUT_PATH = New PersonalUtilities.Forms.Controls.TextBoxExtended() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + TP_BUTTONS = New System.Windows.Forms.TableLayoutPanel() + TP_PLS = New System.Windows.Forms.TableLayoutPanel() + TP_PLS_BUTTONS = New System.Windows.Forms.TableLayoutPanel() + TP_PLS_ITEMS = New System.Windows.Forms.TableLayoutPanel() + TP_SETTINGS = New System.Windows.Forms.TableLayoutPanel() + TP_FORMATS = New System.Windows.Forms.TableLayoutPanel() + LBL_FORMAT = New System.Windows.Forms.Label() + TP_LYRICS = New System.Windows.Forms.TableLayoutPanel() + TT_MAIN = New System.Windows.Forms.ToolTip(Me.components) + TP_MAIN.SuspendLayout() + TP_BUTTONS.SuspendLayout() + CType(Me.SPLITTER_MAIN, System.ComponentModel.ISupportInitialize).BeginInit() + Me.SPLITTER_MAIN.Panel1.SuspendLayout() + Me.SPLITTER_MAIN.Panel2.SuspendLayout() + Me.SPLITTER_MAIN.SuspendLayout() + TP_PLS.SuspendLayout() + TP_PLS_BUTTONS.SuspendLayout() + TP_PLS_ITEMS.SuspendLayout() + TP_SETTINGS.SuspendLayout() + TP_FORMATS.SuspendLayout() + CType(Me.TXT_FORMATS_ADDIT, System.ComponentModel.ISupportInitialize).BeginInit() + TP_LYRICS.SuspendLayout() + CType(Me.TXT_SUBS, System.ComponentModel.ISupportInitialize).BeginInit() + CType(Me.TXT_OUTPUT_PATH, System.ComponentModel.ISupportInitialize).BeginInit() + Me.SuspendLayout() + ' + 'TP_MAIN + ' + TP_MAIN.ColumnCount = 1 + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Controls.Add(TP_BUTTONS, 0, 2) + TP_MAIN.Controls.Add(Me.SPLITTER_MAIN, 0, 1) + TP_MAIN.Controls.Add(TP_SETTINGS, 0, 0) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Margin = New System.Windows.Forms.Padding(0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 3 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 84.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_MAIN.Size = New System.Drawing.Size(434, 261) + TP_MAIN.TabIndex = 0 + ' + 'TP_BUTTONS + ' + TP_BUTTONS.ColumnCount = 3 + TP_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100.0!)) + TP_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 100.0!)) + TP_BUTTONS.Controls.Add(Me.BTT_DOWN, 1, 0) + TP_BUTTONS.Controls.Add(Me.BTT_CANCEL, 2, 0) + TP_BUTTONS.Dock = System.Windows.Forms.DockStyle.Fill + TP_BUTTONS.Location = New System.Drawing.Point(0, 236) + TP_BUTTONS.Margin = New System.Windows.Forms.Padding(0) + TP_BUTTONS.Name = "TP_BUTTONS" + TP_BUTTONS.RowCount = 1 + TP_BUTTONS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_BUTTONS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_BUTTONS.Size = New System.Drawing.Size(434, 25) + TP_BUTTONS.TabIndex = 2 + ' + 'BTT_DOWN + ' + Me.BTT_DOWN.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_DOWN.Location = New System.Drawing.Point(236, 2) + Me.BTT_DOWN.Margin = New System.Windows.Forms.Padding(2) + Me.BTT_DOWN.Name = "BTT_DOWN" + Me.BTT_DOWN.Size = New System.Drawing.Size(96, 21) + Me.BTT_DOWN.TabIndex = 0 + Me.BTT_DOWN.Text = "Download" + Me.BTT_DOWN.UseVisualStyleBackColor = True + ' + 'BTT_CANCEL + ' + Me.BTT_CANCEL.DialogResult = System.Windows.Forms.DialogResult.Cancel + Me.BTT_CANCEL.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_CANCEL.Location = New System.Drawing.Point(336, 2) + Me.BTT_CANCEL.Margin = New System.Windows.Forms.Padding(2) + Me.BTT_CANCEL.Name = "BTT_CANCEL" + Me.BTT_CANCEL.Size = New System.Drawing.Size(96, 21) + Me.BTT_CANCEL.TabIndex = 1 + Me.BTT_CANCEL.Text = "Cancel" + Me.BTT_CANCEL.UseVisualStyleBackColor = True + ' + 'SPLITTER_MAIN + ' + Me.SPLITTER_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.SPLITTER_MAIN.Location = New System.Drawing.Point(3, 87) + Me.SPLITTER_MAIN.Name = "SPLITTER_MAIN" + ' + 'SPLITTER_MAIN.Panel1 + ' + Me.SPLITTER_MAIN.Panel1.Controls.Add(TP_PLS) + Me.SPLITTER_MAIN.Panel1MinSize = 110 + ' + 'SPLITTER_MAIN.Panel2 + ' + Me.SPLITTER_MAIN.Panel2.Controls.Add(TP_PLS_ITEMS) + Me.SPLITTER_MAIN.Size = New System.Drawing.Size(428, 146) + Me.SPLITTER_MAIN.SplitterDistance = 142 + Me.SPLITTER_MAIN.TabIndex = 0 + ' + 'TP_PLS + ' + TP_PLS.ColumnCount = 1 + TP_PLS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_PLS.Controls.Add(Me.LIST_PLAYLISTS, 0, 0) + TP_PLS.Controls.Add(TP_PLS_BUTTONS, 0, 1) + TP_PLS.Dock = System.Windows.Forms.DockStyle.Fill + TP_PLS.Location = New System.Drawing.Point(0, 0) + TP_PLS.Margin = New System.Windows.Forms.Padding(0) + TP_PLS.Name = "TP_PLS" + TP_PLS.RowCount = 2 + TP_PLS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_PLS.Size = New System.Drawing.Size(142, 146) + TP_PLS.TabIndex = 0 + ' + 'LIST_PLAYLISTS + ' + Me.LIST_PLAYLISTS.Dock = System.Windows.Forms.DockStyle.Fill + Me.LIST_PLAYLISTS.FormattingEnabled = True + Me.LIST_PLAYLISTS.Location = New System.Drawing.Point(3, 3) + Me.LIST_PLAYLISTS.Name = "LIST_PLAYLISTS" + Me.LIST_PLAYLISTS.Size = New System.Drawing.Size(136, 115) + Me.LIST_PLAYLISTS.TabIndex = 0 + Me.LIST_PLAYLISTS.ThreeDCheckBoxes = True + ' + 'TP_PLS_BUTTONS + ' + TP_PLS_BUTTONS.ColumnCount = 3 + TP_PLS_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 50.0!)) + TP_PLS_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 50.0!)) + TP_PLS_BUTTONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS_BUTTONS.Controls.Add(Me.BTT_PLS_ALL, 0, 0) + TP_PLS_BUTTONS.Controls.Add(Me.BTT_PLS_NONE, 1, 0) + TP_PLS_BUTTONS.Dock = System.Windows.Forms.DockStyle.Fill + TP_PLS_BUTTONS.Location = New System.Drawing.Point(0, 121) + TP_PLS_BUTTONS.Margin = New System.Windows.Forms.Padding(0) + TP_PLS_BUTTONS.Name = "TP_PLS_BUTTONS" + TP_PLS_BUTTONS.RowCount = 1 + TP_PLS_BUTTONS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS_BUTTONS.Size = New System.Drawing.Size(142, 25) + TP_PLS_BUTTONS.TabIndex = 1 + ' + 'BTT_PLS_ALL + ' + Me.BTT_PLS_ALL.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_PLS_ALL.Location = New System.Drawing.Point(2, 2) + Me.BTT_PLS_ALL.Margin = New System.Windows.Forms.Padding(2) + Me.BTT_PLS_ALL.Name = "BTT_PLS_ALL" + Me.BTT_PLS_ALL.Size = New System.Drawing.Size(46, 21) + Me.BTT_PLS_ALL.TabIndex = 0 + Me.BTT_PLS_ALL.Tag = "a" + Me.BTT_PLS_ALL.Text = "All" + TT_MAIN.SetToolTip(Me.BTT_PLS_ALL, "Select all") + Me.BTT_PLS_ALL.UseVisualStyleBackColor = True + ' + 'BTT_PLS_NONE + ' + Me.BTT_PLS_NONE.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_PLS_NONE.Location = New System.Drawing.Point(52, 2) + Me.BTT_PLS_NONE.Margin = New System.Windows.Forms.Padding(2) + Me.BTT_PLS_NONE.Name = "BTT_PLS_NONE" + Me.BTT_PLS_NONE.Size = New System.Drawing.Size(46, 21) + Me.BTT_PLS_NONE.TabIndex = 1 + Me.BTT_PLS_NONE.Tag = "n" + Me.BTT_PLS_NONE.Text = "None" + TT_MAIN.SetToolTip(Me.BTT_PLS_NONE, "Select none") + Me.BTT_PLS_NONE.UseVisualStyleBackColor = True + ' + 'TP_PLS_ITEMS + ' + TP_PLS_ITEMS.ColumnCount = 1 + TP_PLS_ITEMS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS_ITEMS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_PLS_ITEMS.Controls.Add(Me.LIST_ITEMS, 0, 0) + TP_PLS_ITEMS.Dock = System.Windows.Forms.DockStyle.Fill + TP_PLS_ITEMS.Location = New System.Drawing.Point(0, 0) + TP_PLS_ITEMS.Margin = New System.Windows.Forms.Padding(0) + TP_PLS_ITEMS.Name = "TP_PLS_ITEMS" + TP_PLS_ITEMS.RowCount = 2 + TP_PLS_ITEMS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_PLS_ITEMS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_PLS_ITEMS.Size = New System.Drawing.Size(282, 146) + TP_PLS_ITEMS.TabIndex = 1 + ' + 'LIST_ITEMS + ' + Me.LIST_ITEMS.Dock = System.Windows.Forms.DockStyle.Fill + Me.LIST_ITEMS.FormattingEnabled = True + Me.LIST_ITEMS.Location = New System.Drawing.Point(3, 3) + Me.LIST_ITEMS.Name = "LIST_ITEMS" + Me.LIST_ITEMS.Size = New System.Drawing.Size(276, 115) + Me.LIST_ITEMS.TabIndex = 0 + ' + 'TP_SETTINGS + ' + TP_SETTINGS.ColumnCount = 1 + TP_SETTINGS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_SETTINGS.Controls.Add(TP_FORMATS, 0, 1) + TP_SETTINGS.Controls.Add(TP_LYRICS, 0, 0) + TP_SETTINGS.Controls.Add(Me.TXT_OUTPUT_PATH, 0, 2) + TP_SETTINGS.Dock = System.Windows.Forms.DockStyle.Fill + TP_SETTINGS.Location = New System.Drawing.Point(0, 0) + TP_SETTINGS.Margin = New System.Windows.Forms.Padding(0) + TP_SETTINGS.Name = "TP_SETTINGS" + TP_SETTINGS.RowCount = 3 + TP_SETTINGS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333!)) + TP_SETTINGS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333!)) + TP_SETTINGS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.33333!)) + TP_SETTINGS.Size = New System.Drawing.Size(434, 84) + TP_SETTINGS.TabIndex = 1 + ' + 'TP_FORMATS + ' + TP_FORMATS.ColumnCount = 3 + TP_FORMATS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 50.0!)) + TP_FORMATS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 65.0!)) + TP_FORMATS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_FORMATS.Controls.Add(Me.TXT_FORMATS_ADDIT, 2, 0) + TP_FORMATS.Controls.Add(LBL_FORMAT, 0, 0) + TP_FORMATS.Controls.Add(Me.CMB_FORMATS, 1, 0) + TP_FORMATS.Dock = System.Windows.Forms.DockStyle.Fill + TP_FORMATS.Location = New System.Drawing.Point(0, 28) + TP_FORMATS.Margin = New System.Windows.Forms.Padding(0) + TP_FORMATS.Name = "TP_FORMATS" + TP_FORMATS.RowCount = 1 + TP_FORMATS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_FORMATS.Size = New System.Drawing.Size(434, 28) + TP_FORMATS.TabIndex = 1 + ' + 'TXT_FORMATS_ADDIT + ' + ActionButton1.BackgroundImage = CType(resources.GetObject("ActionButton1.BackgroundImage"), System.Drawing.Image) + ActionButton1.Enabled = False + ActionButton1.Name = "Open" + ActionButton1.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton2.BackgroundImage = CType(resources.GetObject("ActionButton2.BackgroundImage"), System.Drawing.Image) + ActionButton2.Enabled = False + ActionButton2.Name = "Refresh" + ActionButton2.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Refresh + ActionButton3.BackgroundImage = CType(resources.GetObject("ActionButton3.BackgroundImage"), System.Drawing.Image) + ActionButton3.Enabled = False + ActionButton3.Name = "Clear" + ActionButton3.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_FORMATS_ADDIT.Buttons.Add(ActionButton1) + Me.TXT_FORMATS_ADDIT.Buttons.Add(ActionButton2) + Me.TXT_FORMATS_ADDIT.Buttons.Add(ActionButton3) + Me.TXT_FORMATS_ADDIT.CaptionMode = PersonalUtilities.Forms.Controls.Base.ICaptionControl.Modes.CheckBox + Me.TXT_FORMATS_ADDIT.CaptionText = "Additional formats" + Me.TXT_FORMATS_ADDIT.CaptionToolTipEnabled = True + Me.TXT_FORMATS_ADDIT.CaptionToolTipText = "Convert every downloaded track to the formats you choose." + Me.TXT_FORMATS_ADDIT.CaptionWidth = 115.0R + Me.TXT_FORMATS_ADDIT.ClearTextByButtonClear = False + Me.TXT_FORMATS_ADDIT.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_FORMATS_ADDIT.Location = New System.Drawing.Point(118, 3) + Me.TXT_FORMATS_ADDIT.Name = "TXT_FORMATS_ADDIT" + Me.TXT_FORMATS_ADDIT.Size = New System.Drawing.Size(313, 22) + Me.TXT_FORMATS_ADDIT.TabIndex = 2 + Me.TXT_FORMATS_ADDIT.Tag = "a" + Me.TXT_FORMATS_ADDIT.TextBoxReadOnly = True + ' + 'LBL_FORMAT + ' + LBL_FORMAT.Dock = System.Windows.Forms.DockStyle.Fill + LBL_FORMAT.Location = New System.Drawing.Point(3, 0) + LBL_FORMAT.Margin = New System.Windows.Forms.Padding(3, 0, 0, 0) + LBL_FORMAT.Name = "LBL_FORMAT" + LBL_FORMAT.Size = New System.Drawing.Size(47, 28) + LBL_FORMAT.TabIndex = 0 + LBL_FORMAT.Text = "Format:" + LBL_FORMAT.TextAlign = System.Drawing.ContentAlignment.MiddleRight + TT_MAIN.SetToolTip(LBL_FORMAT, "Output files format") + ' + 'CMB_FORMATS + ' + Me.CMB_FORMATS.Dock = System.Windows.Forms.DockStyle.Fill + Me.CMB_FORMATS.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList + Me.CMB_FORMATS.FormattingEnabled = True + Me.CMB_FORMATS.Location = New System.Drawing.Point(53, 3) + Me.CMB_FORMATS.Name = "CMB_FORMATS" + Me.CMB_FORMATS.Size = New System.Drawing.Size(59, 21) + Me.CMB_FORMATS.TabIndex = 1 + ' + 'TP_LYRICS + ' + TP_LYRICS.ColumnCount = 2 + TP_LYRICS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 115.0!)) + TP_LYRICS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_LYRICS.Controls.Add(Me.TXT_SUBS, 1, 0) + TP_LYRICS.Controls.Add(Me.CH_DOWN_LYRICS, 0, 0) + TP_LYRICS.Dock = System.Windows.Forms.DockStyle.Fill + TP_LYRICS.Location = New System.Drawing.Point(0, 0) + TP_LYRICS.Margin = New System.Windows.Forms.Padding(0) + TP_LYRICS.Name = "TP_LYRICS" + TP_LYRICS.RowCount = 1 + TP_LYRICS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_LYRICS.Size = New System.Drawing.Size(434, 28) + TP_LYRICS.TabIndex = 0 + ' + 'TXT_SUBS + ' + ActionButton4.BackgroundImage = CType(resources.GetObject("ActionButton4.BackgroundImage"), System.Drawing.Image) + ActionButton4.Enabled = False + ActionButton4.Name = "Open" + ActionButton4.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton5.BackgroundImage = CType(resources.GetObject("ActionButton5.BackgroundImage"), System.Drawing.Image) + ActionButton5.Enabled = False + ActionButton5.Name = "Refresh" + ActionButton5.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Refresh + ActionButton6.BackgroundImage = CType(resources.GetObject("ActionButton6.BackgroundImage"), System.Drawing.Image) + ActionButton6.Enabled = False + ActionButton6.Name = "Clear" + ActionButton6.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_SUBS.Buttons.Add(ActionButton4) + Me.TXT_SUBS.Buttons.Add(ActionButton5) + Me.TXT_SUBS.Buttons.Add(ActionButton6) + Me.TXT_SUBS.CaptionMode = PersonalUtilities.Forms.Controls.Base.ICaptionControl.Modes.CheckBox + Me.TXT_SUBS.CaptionText = "Additional lyrics" + Me.TXT_SUBS.CaptionToolTipEnabled = True + Me.TXT_SUBS.CaptionToolTipText = "Convert all downloaded lyrics to the formats you choose." + Me.TXT_SUBS.CaptionWidth = 115.0R + Me.TXT_SUBS.ClearTextByButtonClear = False + Me.TXT_SUBS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_SUBS.Location = New System.Drawing.Point(118, 3) + Me.TXT_SUBS.Name = "TXT_SUBS" + Me.TXT_SUBS.Size = New System.Drawing.Size(313, 22) + Me.TXT_SUBS.TabIndex = 1 + Me.TXT_SUBS.Tag = "s" + Me.TXT_SUBS.TextBoxReadOnly = True + ' + 'CH_DOWN_LYRICS + ' + Me.CH_DOWN_LYRICS.AutoSize = True + Me.CH_DOWN_LYRICS.CheckAlign = System.Drawing.ContentAlignment.MiddleRight + Me.CH_DOWN_LYRICS.Dock = System.Windows.Forms.DockStyle.Fill + Me.CH_DOWN_LYRICS.Location = New System.Drawing.Point(3, 3) + Me.CH_DOWN_LYRICS.Name = "CH_DOWN_LYRICS" + Me.CH_DOWN_LYRICS.Size = New System.Drawing.Size(109, 22) + Me.CH_DOWN_LYRICS.TabIndex = 0 + Me.CH_DOWN_LYRICS.Text = "Download lyrics" + Me.CH_DOWN_LYRICS.TextAlign = System.Drawing.ContentAlignment.MiddleRight + TT_MAIN.SetToolTip(Me.CH_DOWN_LYRICS, "Download lyrics if available") + Me.CH_DOWN_LYRICS.UseVisualStyleBackColor = True + ' + 'TXT_OUTPUT_PATH + ' + ActionButton7.BackgroundImage = CType(resources.GetObject("ActionButton7.BackgroundImage"), System.Drawing.Image) + ActionButton7.Name = "Open" + ActionButton7.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton8.BackgroundImage = CType(resources.GetObject("ActionButton8.BackgroundImage"), System.Drawing.Image) + ActionButton8.Name = "Clear" + ActionButton8.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_OUTPUT_PATH.Buttons.Add(ActionButton7) + Me.TXT_OUTPUT_PATH.Buttons.Add(ActionButton8) + Me.TXT_OUTPUT_PATH.CaptionText = "Output path" + Me.TXT_OUTPUT_PATH.CaptionWidth = 112.0R + Me.TXT_OUTPUT_PATH.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_OUTPUT_PATH.Location = New System.Drawing.Point(3, 59) + Me.TXT_OUTPUT_PATH.Name = "TXT_OUTPUT_PATH" + Me.TXT_OUTPUT_PATH.Size = New System.Drawing.Size(428, 22) + Me.TXT_OUTPUT_PATH.TabIndex = 2 + ' + 'MusicPlaylistsForm + ' + Me.AcceptButton = Me.BTT_DOWN + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.CancelButton = Me.BTT_CANCEL + Me.ClientSize = New System.Drawing.Size(434, 261) + Me.Controls.Add(TP_MAIN) + Me.Icon = Global.SCrawler.My.Resources.SiteYouTube.YouTubeMusicIcon_32 + Me.KeyPreview = True + Me.MinimumSize = New System.Drawing.Size(450, 300) + Me.Name = "MusicPlaylistsForm" + Me.Text = "Albums" + TP_MAIN.ResumeLayout(False) + TP_BUTTONS.ResumeLayout(False) + Me.SPLITTER_MAIN.Panel1.ResumeLayout(False) + Me.SPLITTER_MAIN.Panel2.ResumeLayout(False) + CType(Me.SPLITTER_MAIN, System.ComponentModel.ISupportInitialize).EndInit() + Me.SPLITTER_MAIN.ResumeLayout(False) + TP_PLS.ResumeLayout(False) + TP_PLS_BUTTONS.ResumeLayout(False) + TP_PLS_ITEMS.ResumeLayout(False) + TP_SETTINGS.ResumeLayout(False) + TP_FORMATS.ResumeLayout(False) + CType(Me.TXT_FORMATS_ADDIT, System.ComponentModel.ISupportInitialize).EndInit() + TP_LYRICS.ResumeLayout(False) + TP_LYRICS.PerformLayout() + CType(Me.TXT_SUBS, System.ComponentModel.ISupportInitialize).EndInit() + CType(Me.TXT_OUTPUT_PATH, System.ComponentModel.ISupportInitialize).EndInit() + Me.ResumeLayout(False) + + End Sub + Private WithEvents BTT_DOWN As Button + Private WithEvents BTT_CANCEL As Button + Private WithEvents LIST_PLAYLISTS As CheckedListBox + Private WithEvents LIST_ITEMS As CheckedListBox + Private WithEvents TXT_SUBS As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents BTT_PLS_ALL As Button + Private WithEvents BTT_PLS_NONE As Button + Private WithEvents TXT_FORMATS_ADDIT As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents CMB_FORMATS As ComboBox + Private WithEvents SPLITTER_MAIN As SplitContainer + Private WithEvents CH_DOWN_LYRICS As CheckBox + Private WithEvents TXT_OUTPUT_PATH As PersonalUtilities.Forms.Controls.TextBoxExtended + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/MusicPlaylistsForm.resx b/SCrawler.YouTube/Controls/MusicPlaylistsForm.resx new file mode 100644 index 0000000..0a4b79a --- /dev/null +++ b/SCrawler.YouTube/Controls/MusicPlaylistsForm.resx @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + False + + + False + + + False + + + 17, 17 + + + False + + + False + + + False + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElE + QVQ4T2P4//8/QczOJyyqHpzfiE0OQwAZC8iqszAzs7CJ69o4BR768V/W2jcGXQ0KB4aFNS3dDQtnrbCb + ePCK48wTN1wXXXzge/jXf/clV55zC4hIIatF0cjIyMikElzc57z0wX+XHd/+2+//99/ywP//xlu//tdb + +eK/4Zp3/1WTOhYzARViNUAluKjTdf37/0ZTTn9TbdhwXblhwwW1/qOP1Ja9+K8w+95/6cm3/6v2Xvkv + qKjniGGAoIqRpW3/4e8S9uGdzFz82gwMDFxAzCxm4ZegtuLDf+VJ1/8rZM25IqLvnM/CximCYYCic1QN + v7x2JIwPwyrJ3XNUylddE9G2TWNmZOBDl4czmJiZMSRBmFdSyYyJgUEQmxwIYxWEYXZBCUls4sgYq6CA + prWNbtG8nXKeaVPR5XiVjSxEzf0yYXy4BBMLO6eQjoOXZvrkbbazrv53Xf/2v4CSbjBMXkhBl1/CMyNZ + qWnvGy5pNQ+YONwAfjXzAOupl/47LLr333L50/96q9/8l23YdES6cO5KuYqVW+R7Tj6SnfP0v4hryjyY + HhQDmFjYeHVKFp7WX/Xuv9Kq9/+Vd/z7r7rv/3+l7f//y676DEwDN/9L+BVvYkKLCTgDhNkkVUyVlr74 + qbbz73/VOTc/qsy89kWx+9h7qbQpJwS1bbOAscGGrB6EUTggLOqf16C55ft/HlnNAFZOXgVWdi4FRgYG + VnR1MIwhwMTCyqEQ37qEmZVDFF0OE/9nAACtFF4Ey6OP+wAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + False + + + False + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElE + QVQ4T2P4//8/QczOJyyqHpzfiE0OQwAZC8iqszAzs7CJ69o4BR768V/W2jcGXQ0KB4aFNS3dDQtnrbCb + ePCK48wTN1wXXXzge/jXf/clV55zC4hIIatF0cjIyMikElzc57z0wX+XHd/+2+//99/ywP//xlu//tdb + +eK/4Zp3/1WTOhYzARViNUAluKjTdf37/0ZTTn9TbdhwXblhwwW1/qOP1Ja9+K8w+95/6cm3/6v2Xvkv + qKjniGGAoIqRpW3/4e8S9uGdzFz82gwMDFxAzCxm4ZegtuLDf+VJ1/8rZM25IqLvnM/CximCYYCic1QN + v7x2JIwPwyrJ3XNUylddE9G2TWNmZOBDl4czmJiZMSRBmFdSyYyJgUEQmxwIYxWEYXZBCUls4sgYq6CA + prWNbtG8nXKeaVPR5XiVjSxEzf0yYXy4BBMLO6eQjoOXZvrkbbazrv53Xf/2v4CSbjBMXkhBl1/CMyNZ + qWnvGy5pNQ+YONwAfjXzAOupl/47LLr333L50/96q9/8l23YdES6cO5KuYqVW+R7Tj6SnfP0v4hryjyY + HhQDmFjYeHVKFp7WX/Xuv9Kq9/+Vd/z7r7rv/3+l7f//y676DEwDN/9L+BVvYkKLCTgDhNkkVUyVlr74 + qbbz73/VOTc/qsy89kWx+9h7qbQpJwS1bbOAscGGrB6EUTggLOqf16C55ft/HlnNAFZOXgVWdi4FRgYG + VnR1MIwhwMTCyqEQ37qEmZVDFF0OE/9nAACtFF4Ey6OP+wAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/MusicPlaylistsForm.vb b/SCrawler.YouTube/Controls/MusicPlaylistsForm.vb new file mode 100644 index 0000000..9247bca --- /dev/null +++ b/SCrawler.YouTube/Controls/MusicPlaylistsForm.vb @@ -0,0 +1,262 @@ +' 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.ComponentModel +Imports SCrawler.API.YouTube.Objects +Imports PersonalUtilities.Forms +Imports PersonalUtilities.Forms.Controls +Imports PersonalUtilities.Forms.Controls.Base +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports ADB = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons +Namespace API.YouTube.Controls + Friend Class MusicPlaylistsForm : Implements IDesignXMLContainer +#Region "Declarations" + Private MyView As FormView + Friend Property DesignXML As EContainer Implements IDesignXMLContainer.DesignXML + Private Property DesignXMLNodes As String() Implements IDesignXMLContainer.DesignXMLNodes + Private Property DesignXMLNodeName As String Implements IDesignXMLContainer.DesignXMLNodeName + Private ReadOnly MyContainer As IYouTubeMediaContainer + Private Initializing As Boolean = True + Private ReadOnly Property Current As IYouTubeMediaContainer + Get + With MyContainer + If .ObjectType = Base.YouTubeMediaType.Channel Then + If _LatestSelected.ValueBetween(0, .Count - 1) Then Return .Elements(_LatestSelected) + Else + Return .Self + End If + End With + Return Nothing + End Get + End Property +#End Region +#Region "Initializer" + Friend Sub New(ByVal Container As IYouTubeMediaContainer) + InitializeComponent() + MyContainer = Container + End Sub +#End Region +#Region "Form handlers" + Private Sub MusicPlaylistsForm_Load(sender As Object, e As EventArgs) Handles Me.Load + If Not DesignXML Is Nothing Then + MyView = New FormView(Me) + MyView.Import() + MyView.SetFormSize() + End If + + CMB_FORMATS.Items.AddRange(AvailableAudioFormats) + If MyYouTubeSettings.PlaylistFormSplitterDistance > 0 Then SPLITTER_MAIN.SplitterDistancePercentageSet(MyYouTubeSettings.PlaylistFormSplitterDistance) + + With MyContainer + CH_DOWN_LYRICS.Checked = Not .OutputSubtitlesFormat.IsEmptyString + Dim i% + If Not .OutputAudioCodec.IsEmptyString Then + i = AvailableAudioFormats.ListIndexOf(Function(ff) ff.StringToLower = .OutputAudioCodec.StringToLower) + If i >= 0 Then CMB_FORMATS.SelectedIndex = i + End If + If CMB_FORMATS.SelectedIndex = -1 Then + Dim oac$ = MyYouTubeSettings.DefaultAudioCodecMusic.Value.IfNullOrEmpty("mp3").StringToLower + i = AvailableAudioFormats.ListIndexOf(Function(ff) ff.StringToLower = oac) + If i >= 0 Then CMB_FORMATS.SelectedIndex = i Else CMB_FORMATS.SelectedIndex = 0 + End If + + If .ObjectType = Base.YouTubeMediaType.Channel Then + If .HasElements Then + For Each elem In .Elements : LIST_PLAYLISTS.Items.Add(elem, elem.CheckState) : Next + End If + ElseIf .ObjectType = Base.YouTubeMediaType.PlayList Then + LIST_PLAYLISTS.Items.Add(.Self, .CheckState) + Else + Throw New InvalidOperationException($"The object type '{ .ObjectType}' is incompatible with 'MusicPlaylistsForm'.") + End If + LIST_PLAYLISTS.SelectedIndex = 0 + + TXT_OUTPUT_PATH.Text = MyYouTubeSettings.OutputPath.Value + + If Not .UserTitle.IsEmptyString Then + Text = .UserTitle + ElseIf Not .PlaylistTitle.IsEmptyString Then + Text = .PlaylistTitle + End If + + UpdateSizeText() + End With + RefillAddit() + Initializing = False + End Sub + Private Sub MusicPlaylistsForm_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing + MyYouTubeSettings.PlaylistFormSplitterDistance.Value = SPLITTER_MAIN.SplitterDistancePercentageGet + MyView.DisposeIfReady() + End Sub +#End Region +#Region "Form text" + Private _InitialFormText As String = String.Empty + Private Sub UpdateSizeText() + If _InitialFormText.IsEmptyString Then _InitialFormText = Text + Text = $"{_InitialFormText} ({MyContainer.SizeStr})" + End Sub +#End Region +#Region "Settings" + Private Sub TXT_SUBS_ActionOnButtonClick(ByVal Sender As ActionButton, ByVal e As ActionButtonEventArgs) Handles TXT_SUBS.ActionOnButtonClick, TXT_FORMATS_ADDIT.ActionOnButtonClick + Dim isLyrics As Boolean = DirectCast(e.AssociatedControl, Control).Tag = "s" + With DirectCast(MyContainer, YouTubeMediaContainerBase) + Select Case Sender.DefaultButton + Case ADB.Open + Using f As New SimpleListForm(Of String)(IIf(isLyrics, AvailableSubtitlesFormats, AvailableAudioFormats)) With { + .DesignXML = DesignXML, + .DesignXMLNodeName = SimpleArraysFormNode, + .FormText = DirectCast(e.AssociatedControl, TextBoxExtended).CaptionText, + .Icon = My.Resources.SiteYouTube.YouTubeMusicIcon_32, + .Mode = SimpleListFormModes.CheckedItems + } + f.DataSelected.ListAddList(IIf(isLyrics, .PostProcessing_OutputSubtitlesFormats, .PostProcessing_OutputAudioFormats)) + If f.ShowDialog = DialogResult.OK Then + If isLyrics Then + .PostProcessing_OutputSubtitlesFormats.Clear() + .PostProcessing_OutputSubtitlesFormats.ListAddList(f.DataResult) + Else + .PostProcessing_OutputAudioFormats.Clear() + .PostProcessing_OutputAudioFormats.ListAddList(f.DataResult) + End If + RefillAddit() + End If + End Using + Case ADB.Refresh + If isLyrics Then + .PostProcessing_OutputSubtitlesFormats_Reset() + Else + .PostProcessing_OutputAudioFormats_Reset() + End If + RefillAddit() + Case ADB.Clear + If isLyrics Then + .PostProcessing_OutputSubtitlesFormats.Clear() + Else + .PostProcessing_OutputAudioFormats.Clear() + End If + RefillAddit() + End Select + End With + End Sub + Private Sub RefillAddit() + With DirectCast(MyContainer, YouTubeMediaContainerBase) + TXT_SUBS.Text = .PostProcessing_OutputSubtitlesFormats.ListToString + TXT_FORMATS_ADDIT.Text = .PostProcessing_OutputAudioFormats.ListToString + End With + End Sub + Private Sub TXT_OUTPUT_PATH_ActionOnButtonClick(ByVal Sender As ActionButton, ByVal e As ActionButtonEventArgs) Handles TXT_OUTPUT_PATH.ActionOnButtonClick + If Sender.DefaultButton = ADB.Open Then + Dim f As SFile = SFile.SelectPath(TXT_OUTPUT_PATH.Text, "Select files destination", EDP.ReturnValue) + If Not f.IsEmptyString Then TXT_OUTPUT_PATH.Text = f + End If + End Sub +#End Region +#Region "Lists' handlers" + Private _LatestSelected As Integer = -1 + Private Sub LIST_PLAYLISTS_SelectedIndexChanged(sender As Object, e As EventArgs) Handles LIST_PLAYLISTS.SelectedIndexChanged + Dim i% = LIST_PLAYLISTS.SelectedIndex + If i >= 0 Then + _LatestSelected = i + LIST_ITEMS.Items.Clear() + With DirectCast(LIST_PLAYLISTS.SelectedItem, IYouTubeMediaContainer) + If .HasElements Then + For Each elem In .Elements : LIST_ITEMS.Items.Add(elem, elem.Checked) : Next + End If + End With + End If + End Sub + Private _CheckHandlersSuspended As Boolean = False + Private Sub LIST_PLAYLISTS_ItemCheck(sender As Object, e As ItemCheckEventArgs) Handles LIST_PLAYLISTS.ItemCheck + If Not Initializing And Not _CheckHandlersSuspended Then + _CheckHandlersSuspended = True + + Dim checked As Boolean = Not e.NewValue = CheckState.Unchecked + + Dim current As IYouTubeMediaContainer = Me.Current + If Not current Is Nothing Then + With current + .Checked = checked + If LIST_ITEMS.Items.Count > 0 Then + _ListCheckHandlersSuspended = True + For i% = 0 To .Count - 1 + If i.ValueBetween(0, LIST_ITEMS.Items.Count - 1) Then LIST_ITEMS.SetItemChecked(i, checked) + Next + _ListCheckHandlersSuspended = False + End If + If LIST_PLAYLISTS.Items.Count.ValueBetween(0, _LatestSelected) Then LIST_PLAYLISTS.SetItemChecked(_LatestSelected, checked) + End With + UpdateSizeText() + End If + + _CheckHandlersSuspended = False + End If + End Sub + Private _ListCheckHandlersSuspended As Boolean = False + Private Sub LIST_ITEMS_ItemCheck(sender As Object, e As ItemCheckEventArgs) Handles LIST_ITEMS.ItemCheck + If Not Initializing Then + Dim current As IYouTubeMediaContainer = Me.Current + If Not current Is Nothing Then + With current + If e.Index.ValueBetween(0, .Count - 1) Then + .Elements(e.Index).Checked = e.NewValue + If Not _ListCheckHandlersSuspended And _LatestSelected.ValueBetween(0, LIST_PLAYLISTS.Items.Count - 1) Then + Dim checked As Boolean = .Elements(e.Index).Checked + _CheckHandlersSuspended = True + If .Elements.All(Function(ee) ee.Checked = checked) Then + LIST_PLAYLISTS.SetItemChecked(_LatestSelected, checked) + Else + LIST_PLAYLISTS.SetItemCheckState(_LatestSelected, CheckState.Indeterminate) + End If + _CheckHandlersSuspended = False + LIST_PLAYLISTS.Refresh() + UpdateSizeText() + End If + End If + End With + End If + End If + End Sub +#End Region +#Region "Selection buttons" + Private Sub BTT_PLS_Click(sender As Object, e As EventArgs) Handles BTT_PLS_ALL.Click, BTT_PLS_NONE.Click + Dim checked As Boolean = DirectCast(sender, Button).Tag = "a" + _CheckHandlersSuspended = True + If LIST_PLAYLISTS.Items.Count > 0 Then + For i% = 0 To LIST_PLAYLISTS.Items.Count - 1 : LIST_PLAYLISTS.SetItemChecked(i, checked) : Next + End If + MyContainer.Checked = checked + If MyContainer.Count > 1 Then MyContainer.Elements.ForEach(Sub(ee) ee.Checked = checked) + _CheckHandlersSuspended = False + UpdateSizeText() + End Sub +#End Region +#Region "Ok, Cancel" + Private Sub BTT_DOWN_Click(sender As Object, e As EventArgs) Handles BTT_DOWN.Click + If TXT_OUTPUT_PATH.IsEmptyString Then + MsgBoxE({"The output path cannot be null.", "Download music"}, vbCritical) + Else + With DirectCast(MyContainer, YouTubeMediaContainerBase) + .OutputSubtitlesFormat = IIf(CH_DOWN_LYRICS.Checked, "LRC", String.Empty) + If Not TXT_SUBS.Checked Then .PostProcessing_OutputSubtitlesFormats.Clear() + .OutputAudioCodec = CMB_FORMATS.Text + If Not TXT_FORMATS_ADDIT.Checked Then .PostProcessing_OutputAudioFormats.Clear() + .File = TXT_OUTPUT_PATH.Text.CSFileP + If MyYouTubeSettings.OutputPathAutoChange Then MyYouTubeSettings.OutputPath.Value = .File + End With + DialogResult = DialogResult.OK + Close() + End If + End Sub + Private Sub BTT_CANCEL_Click(sender As Object, e As EventArgs) Handles BTT_CANCEL.Click + DialogResult = DialogResult.Cancel + Close() + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/ParsingProgressForm.Designer.vb b/SCrawler.YouTube/Controls/ParsingProgressForm.Designer.vb new file mode 100644 index 0000000..96ab149 --- /dev/null +++ b/SCrawler.YouTube/Controls/ParsingProgressForm.Designer.vb @@ -0,0 +1,94 @@ +' 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.YouTube.Controls + + Partial Public Class ParsingProgressForm : 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 TP_MAIN As System.Windows.Forms.TableLayoutPanel + Me.PR_MAIN = New System.Windows.Forms.ProgressBar() + Me.LBL_MAIN = New System.Windows.Forms.Label() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + TP_MAIN.SuspendLayout() + Me.SuspendLayout() + ' + 'TP_MAIN + ' + 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.PR_MAIN, 0, 0) + TP_MAIN.Controls.Add(Me.LBL_MAIN, 0, 1) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Margin = New System.Windows.Forms.Padding(0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 2 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + TP_MAIN.Size = New System.Drawing.Size(334, 56) + TP_MAIN.TabIndex = 0 + ' + 'PR_MAIN + ' + Me.PR_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.PR_MAIN.Location = New System.Drawing.Point(3, 3) + Me.PR_MAIN.Name = "PR_MAIN" + Me.PR_MAIN.Size = New System.Drawing.Size(328, 25) + Me.PR_MAIN.TabIndex = 0 + ' + 'LBL_MAIN + ' + Me.LBL_MAIN.AutoSize = True + Me.LBL_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_MAIN.Location = New System.Drawing.Point(3, 31) + Me.LBL_MAIN.Name = "LBL_MAIN" + Me.LBL_MAIN.Size = New System.Drawing.Size(328, 25) + Me.LBL_MAIN.TabIndex = 1 + Me.LBL_MAIN.TextAlign = System.Drawing.ContentAlignment.TopCenter + ' + 'ParsingProgressForm + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(334, 56) + Me.ControlBox = False + Me.Controls.Add(TP_MAIN) + Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow + Me.KeyPreview = True + Me.MaximizeBox = False + Me.MaximumSize = New System.Drawing.Size(350, 95) + Me.MinimizeBox = False + Me.MinimumSize = New System.Drawing.Size(350, 95) + Me.Name = "ParsingProgressForm" + Me.ShowIcon = False + Me.ShowInTaskbar = False + Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide + Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent + Me.Text = "Parsing progress" + Me.TopMost = True + TP_MAIN.ResumeLayout(False) + TP_MAIN.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Private WithEvents PR_MAIN As ProgressBar + Private WithEvents LBL_MAIN As Label + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/OptionsForm.resx b/SCrawler.YouTube/Controls/ParsingProgressForm.resx similarity index 96% rename from SCrawler/API/PornHub/OptionsForm.resx rename to SCrawler.YouTube/Controls/ParsingProgressForm.resx index be8e932..e5f5c30 100644 --- a/SCrawler/API/PornHub/OptionsForm.resx +++ b/SCrawler.YouTube/Controls/ParsingProgressForm.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - False - False diff --git a/SCrawler.YouTube/Controls/ParsingProgressForm.vb b/SCrawler.YouTube/Controls/ParsingProgressForm.vb new file mode 100644 index 0000000..d9e249d --- /dev/null +++ b/SCrawler.YouTube/Controls/ParsingProgressForm.vb @@ -0,0 +1,48 @@ +' 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 PersonalUtilities.Forms.Toolbars +Namespace API.YouTube.Controls + Public Class ParsingProgressForm + Public ReadOnly Property MyProgress As MyProgress + Private ReadOnly TokenSource As CancellationTokenSource + Public ReadOnly Property Token As CancellationToken + Get + Return TokenSource.Token + End Get + End Property + Public Sub New() + InitializeComponent() + MyProgress = New MyProgress(PR_MAIN, LBL_MAIN, "Data parsing in progress") With {.ResetProgressOnMaximumChanges = False} + TokenSource = New CancellationTokenSource + End Sub + Public Sub SetInitialValues(ByVal Count As Integer, ByVal Info As String) + With MyProgress + .Maximum = Count + .Visible = True + .Perform(0.5) + .Value = 0 + .InformationTemporary = Info + End With + End Sub + Private _KeyDownDisabled As Boolean = False + Private Sub ParsingProgressForm_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown + If e.KeyCode = Keys.Escape AndAlso Not _KeyDownDisabled AndAlso MsgBoxE({"Data parsing in progress." & vbCr & + "Are you sure you want to stop parsing and cancel the operation?", + "Stop parsing"}, vbExclamation + vbYesNo) = vbYes Then + _KeyDownDisabled = True + TokenSource.Cancel() + End If + End Sub + Private Sub ParsingProgressForm_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed + MyProgress.Dispose() + TokenSource.Dispose() + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/PlayListParserForm.Designer.vb b/SCrawler.YouTube/Controls/PlayListParserForm.Designer.vb new file mode 100644 index 0000000..1d6483e --- /dev/null +++ b/SCrawler.YouTube/Controls/PlayListParserForm.Designer.vb @@ -0,0 +1,171 @@ +' 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.YouTube.Controls + + Partial Friend Class PlayListParserForm : 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() + Me.components = New System.ComponentModel.Container() + Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel + Dim FRM_IN As System.Windows.Forms.GroupBox + Dim FRM_OUT As System.Windows.Forms.GroupBox + Dim ActionButton1 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(PlayListParserForm)) + Dim CONTAINER_MAIN As System.Windows.Forms.ToolStripContainer + Dim TT_MAIN As System.Windows.Forms.ToolTip + Me.TXT_IN = New System.Windows.Forms.RichTextBox() + Me.TXT_OUT = New System.Windows.Forms.RichTextBox() + Me.TXT_LIMIT = New PersonalUtilities.Forms.Controls.TextBoxExtended() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + FRM_IN = New System.Windows.Forms.GroupBox() + FRM_OUT = New System.Windows.Forms.GroupBox() + CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() + TT_MAIN = New System.Windows.Forms.ToolTip(Me.components) + TP_MAIN.SuspendLayout() + FRM_IN.SuspendLayout() + FRM_OUT.SuspendLayout() + CType(Me.TXT_LIMIT, System.ComponentModel.ISupportInitialize).BeginInit() + CONTAINER_MAIN.ContentPanel.SuspendLayout() + CONTAINER_MAIN.SuspendLayout() + Me.SuspendLayout() + ' + 'TP_MAIN + ' + TP_MAIN.ColumnCount = 1 + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Controls.Add(FRM_IN, 0, 1) + TP_MAIN.Controls.Add(FRM_OUT, 0, 2) + TP_MAIN.Controls.Add(Me.TXT_LIMIT, 0, 0) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Margin = New System.Windows.Forms.Padding(0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 3 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_MAIN.Size = New System.Drawing.Size(384, 261) + TP_MAIN.TabIndex = 0 + ' + 'FRM_IN + ' + FRM_IN.Controls.Add(Me.TXT_IN) + FRM_IN.Dock = System.Windows.Forms.DockStyle.Fill + FRM_IN.Location = New System.Drawing.Point(3, 31) + FRM_IN.Name = "FRM_IN" + FRM_IN.Size = New System.Drawing.Size(378, 110) + FRM_IN.TabIndex = 0 + FRM_IN.TabStop = False + FRM_IN.Text = "In" + TT_MAIN.SetToolTip(FRM_IN, "In your browser's DevTools, find the page starting with the following URL and cop" & + "y the response text into this window." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "https://music.youtube.com/youtubei/v1/bro" & + "wse?key=") + ' + 'TXT_IN + ' + Me.TXT_IN.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_IN.Location = New System.Drawing.Point(3, 16) + Me.TXT_IN.Name = "TXT_IN" + Me.TXT_IN.Size = New System.Drawing.Size(372, 91) + Me.TXT_IN.TabIndex = 0 + Me.TXT_IN.Text = "" + ' + 'FRM_OUT + ' + FRM_OUT.Controls.Add(Me.TXT_OUT) + FRM_OUT.Dock = System.Windows.Forms.DockStyle.Fill + FRM_OUT.Location = New System.Drawing.Point(3, 147) + FRM_OUT.Name = "FRM_OUT" + FRM_OUT.Size = New System.Drawing.Size(378, 111) + FRM_OUT.TabIndex = 1 + FRM_OUT.TabStop = False + FRM_OUT.Text = "Out" + ' + 'TXT_OUT + ' + Me.TXT_OUT.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_OUT.Location = New System.Drawing.Point(3, 16) + Me.TXT_OUT.Name = "TXT_OUT" + Me.TXT_OUT.Size = New System.Drawing.Size(372, 92) + Me.TXT_OUT.TabIndex = 0 + Me.TXT_OUT.Text = "" + ' + 'TXT_LIMIT + ' + ActionButton1.BackgroundImage = CType(resources.GetObject("ActionButton1.BackgroundImage"), System.Drawing.Image) + ActionButton1.Name = "Clear" + ActionButton1.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_LIMIT.Buttons.Add(ActionButton1) + Me.TXT_LIMIT.CaptionText = "Remove" + Me.TXT_LIMIT.CaptionToolTipEnabled = True + Me.TXT_LIMIT.CaptionToolTipText = "Remove playlists starts with..." + Me.TXT_LIMIT.CaptionWidth = 50.0R + Me.TXT_LIMIT.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_LIMIT.Location = New System.Drawing.Point(3, 3) + Me.TXT_LIMIT.Name = "TXT_LIMIT" + Me.TXT_LIMIT.PlaceholderEnabled = True + Me.TXT_LIMIT.PlaceholderText = "e.g. ABCDE" + Me.TXT_LIMIT.Size = New System.Drawing.Size(378, 22) + Me.TXT_LIMIT.TabIndex = 2 + ' + 'CONTAINER_MAIN + ' + ' + 'CONTAINER_MAIN.ContentPanel + ' + CONTAINER_MAIN.ContentPanel.Controls.Add(TP_MAIN) + CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(384, 261) + 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, 261) + CONTAINER_MAIN.TabIndex = 0 + CONTAINER_MAIN.TopToolStripPanelVisible = False + ' + 'PlayListParserForm + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(384, 261) + Me.Controls.Add(CONTAINER_MAIN) + Me.Icon = Global.SCrawler.My.Resources.SiteYouTube.YouTubeMusicIcon_32 + Me.KeyPreview = True + Me.MinimizeBox = False + Me.MinimumSize = New System.Drawing.Size(400, 300) + Me.Name = "PlayListParserForm" + Me.ShowInTaskbar = False + Me.Text = "Playlist parser" + TP_MAIN.ResumeLayout(False) + FRM_IN.ResumeLayout(False) + FRM_OUT.ResumeLayout(False) + CType(Me.TXT_LIMIT, System.ComponentModel.ISupportInitialize).EndInit() + CONTAINER_MAIN.ContentPanel.ResumeLayout(False) + CONTAINER_MAIN.ResumeLayout(False) + CONTAINER_MAIN.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Private WithEvents TXT_IN As RichTextBox + Private WithEvents TXT_OUT As RichTextBox + Private WithEvents TXT_LIMIT As PersonalUtilities.Forms.Controls.TextBoxExtended + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/PlayListParserForm.resx b/SCrawler.YouTube/Controls/PlayListParserForm.resx new file mode 100644 index 0000000..bf08091 --- /dev/null +++ b/SCrawler.YouTube/Controls/PlayListParserForm.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + False + + + 17, 17 + + + False + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + False + + \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/PlayListParserForm.vb b/SCrawler.YouTube/Controls/PlayListParserForm.vb new file mode 100644 index 0000000..1d82e9e --- /dev/null +++ b/SCrawler.YouTube/Controls/PlayListParserForm.vb @@ -0,0 +1,60 @@ +' 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.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Functions.RegularExpressions +Namespace API.YouTube.Controls + Friend Class PlayListParserForm : Implements IDesignXMLContainer + Private WithEvents MyDefs As DefaultFormOptions + Friend Property DesignXML As EContainer Implements IDesignXMLContainer.DesignXML + Private Property DesignXMLNodes As String() Implements IDesignXMLContainer.DesignXMLNodes + Private Property DesignXMLNodeName As String Implements IDesignXMLContainer.DesignXMLNodeName + Friend ReadOnly Property PlayLists As List(Of String) + Get + If Not TXT_OUT.Text.IsEmptyString Then + Return TXT_OUT.Lines.ToList + Else + Return New List(Of String) + End If + End Get + End Property + Friend Sub New() + InitializeComponent() + MyDefs = New DefaultFormOptions(Me) + End Sub + Private Sub MyForm_Load(sender As Object, e As EventArgs) Handles Me.Load + With MyDefs + .MyViewInitialize() + .AddOkCancelToolbar() + .EndLoaderOperations() + End With + End Sub + Private Sub UpdatePlaylists(sender As Object, e As EventArgs) Handles TXT_IN.TextChanged, TXT_LIMIT.ActionOnTextChanged + Try + If Not TXT_IN.Text.IsEmptyString Then + Dim l As List(Of String) = ListAddList(Of String)(Nothing, RegexReplace(TXT_IN.Text, RParams.DMS("playlistId"": ""([^""]+)""", 1, + RegexReturn.List, EDP.ReturnValue)), + LAP.NotContainsOnly, EDP.ReturnValue) + If Not TXT_LIMIT.Text.IsEmptyString And l.ListExists Then l.RemoveAll(Function(id) id.StringToLower.StartsWith(TXT_LIMIT.Text.ToLower)) + If l.ListExists Then + TXT_OUT.Text = l.Select(Function(id) $"https://music.youtube.com/playlist?list={id}").ListToString(vbNewLine, EDP.ReturnValue) + Else + TXT_OUT.Text = String.Empty + End If + Else + TXT_OUT.Text = String.Empty + End If + Catch ex As Exception + TXT_OUT.Text = String.Empty + ErrorsDescriber.Execute(EDP.LogMessageValue, ex, "[PlayListParserForm.UpdatePlaylists]") + End Try + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/OptionsForm.Designer.vb b/SCrawler.YouTube/Controls/PlaylistArrayForm.Designer.vb similarity index 55% rename from SCrawler/API/PornHub/OptionsForm.Designer.vb rename to SCrawler.YouTube/Controls/PlaylistArrayForm.Designer.vb index d1b0953..7bcfa45 100644 --- a/SCrawler/API/PornHub/OptionsForm.Designer.vb +++ b/SCrawler.YouTube/Controls/PlaylistArrayForm.Designer.vb @@ -6,9 +6,9 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY -Namespace API.PornHub +Namespace API.YouTube.Controls - Partial Friend Class OptionsForm : Inherits System.Windows.Forms.Form + Partial Friend Class PlaylistArrayForm : Inherits System.Windows.Forms.Form Protected Overrides Sub Dispose(ByVal disposing As Boolean) Try @@ -24,13 +24,16 @@ Namespace API.PornHub 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() + Dim FRM_PLS As System.Windows.Forms.GroupBox + Me.CH_PLS_ONE = New System.Windows.Forms.CheckBox() + Me.TXT_URLS = New System.Windows.Forms.RichTextBox() CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + FRM_PLS = New System.Windows.Forms.GroupBox() CONTAINER_MAIN.ContentPanel.SuspendLayout() CONTAINER_MAIN.SuspendLayout() TP_MAIN.SuspendLayout() + FRM_PLS.SuspendLayout() Me.SuspendLayout() ' 'CONTAINER_MAIN @@ -39,80 +42,87 @@ Namespace API.PornHub 'CONTAINER_MAIN.ContentPanel ' CONTAINER_MAIN.ContentPanel.Controls.Add(TP_MAIN) - CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(278, 52) + CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(384, 311) 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.Size = New System.Drawing.Size(384, 311) 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.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_MAIN.Controls.Add(Me.CH_PLS_ONE, 0, 0) + TP_MAIN.Controls.Add(FRM_PLS, 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.RowCount = 2 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.Size = New System.Drawing.Size(384, 311) TP_MAIN.TabIndex = 0 ' - 'CH_DOWN_GIFS + 'CH_PLS_ONE ' - 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 + Me.CH_PLS_ONE.AutoSize = True + Me.CH_PLS_ONE.Checked = True + Me.CH_PLS_ONE.CheckState = System.Windows.Forms.CheckState.Checked + Me.CH_PLS_ONE.Dock = System.Windows.Forms.DockStyle.Fill + Me.CH_PLS_ONE.Location = New System.Drawing.Point(3, 3) + Me.CH_PLS_ONE.Name = "CH_PLS_ONE" + Me.CH_PLS_ONE.Size = New System.Drawing.Size(378, 19) + Me.CH_PLS_ONE.TabIndex = 1 + Me.CH_PLS_ONE.Text = "Playlists / Albums by one artist" + Me.CH_PLS_ONE.UseVisualStyleBackColor = True ' - 'CH_DOWN_PHOTO_MODELHUB + 'FRM_PLS ' - 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 + FRM_PLS.Controls.Add(Me.TXT_URLS) + FRM_PLS.Dock = System.Windows.Forms.DockStyle.Fill + FRM_PLS.Location = New System.Drawing.Point(3, 28) + FRM_PLS.Name = "FRM_PLS" + FRM_PLS.Size = New System.Drawing.Size(378, 280) + FRM_PLS.TabIndex = 0 + FRM_PLS.TabStop = False + FRM_PLS.Text = "URLs (new line as delimiter); Ctrl+O to parse playlists from response" ' - 'OptionsForm + 'TXT_URLS + ' + Me.TXT_URLS.DetectUrls = False + Me.TXT_URLS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_URLS.Location = New System.Drawing.Point(3, 16) + Me.TXT_URLS.Name = "TXT_URLS" + Me.TXT_URLS.Size = New System.Drawing.Size(372, 261) + Me.TXT_URLS.TabIndex = 0 + Me.TXT_URLS.Text = "" + ' + 'PlaylistArrayForm ' 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.ClientSize = New System.Drawing.Size(384, 311) Me.Controls.Add(CONTAINER_MAIN) - Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle - Me.Icon = Global.SCrawler.My.Resources.SiteResources.InstagramIcon_32 + Me.Icon = Global.SCrawler.My.Resources.SiteYouTube.YouTubeMusicIcon_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" + Me.MinimumSize = New System.Drawing.Size(400, 350) + Me.Name = "PlaylistArrayForm" + Me.Text = "Playlists / Albums" CONTAINER_MAIN.ContentPanel.ResumeLayout(False) CONTAINER_MAIN.ResumeLayout(False) CONTAINER_MAIN.PerformLayout() TP_MAIN.ResumeLayout(False) TP_MAIN.PerformLayout() + FRM_PLS.ResumeLayout(False) Me.ResumeLayout(False) End Sub - Private WithEvents CH_DOWN_GIFS As CheckBox - Private WithEvents CH_DOWN_PHOTO_MODELHUB As CheckBox + Private WithEvents CH_PLS_ONE As CheckBox + Private WithEvents TXT_URLS As RichTextBox End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Instagram/OptionsForm.resx b/SCrawler.YouTube/Controls/PlaylistArrayForm.resx similarity index 97% rename from SCrawler/API/Instagram/OptionsForm.resx rename to SCrawler.YouTube/Controls/PlaylistArrayForm.resx index be8e932..58e44b8 100644 --- a/SCrawler/API/Instagram/OptionsForm.resx +++ b/SCrawler.YouTube/Controls/PlaylistArrayForm.resx @@ -123,4 +123,7 @@ False + + False + \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/PlaylistArrayForm.vb b/SCrawler.YouTube/Controls/PlaylistArrayForm.vb new file mode 100644 index 0000000..0d137ed --- /dev/null +++ b/SCrawler.YouTube/Controls/PlaylistArrayForm.vb @@ -0,0 +1,54 @@ +' 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.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Namespace API.YouTube.Controls + Friend Class PlaylistArrayForm : Implements IDesignXMLContainer + Private WithEvents MyDefs As DefaultFormOptions + Friend Property DesignXML As EContainer Implements IDesignXMLContainer.DesignXML + Private Property DesignXMLNodes As String() Implements IDesignXMLContainer.DesignXMLNodes + Private Property DesignXMLNodeName As String Implements IDesignXMLContainer.DesignXMLNodeName + Friend ReadOnly Property URLs As List(Of String) + Get + If Not TXT_URLS.Text.IsEmptyString Then + Return ListAddList(Nothing, TXT_URLS.Text.StringFormatLines.StringToList(Of String)(vbNewLine), + LAP.NotContainsOnly, EDP.ReturnValue, CType(Function(Input$) Input.StringTrim, Func(Of Object, Object))).ListIfNothing + Else + Return New List(Of String) + End If + End Get + End Property + Friend ReadOnly Property IsOneArtist As Boolean + Get + Return CH_PLS_ONE.Checked + End Get + End Property + Friend Sub New() + InitializeComponent() + MyDefs = New DefaultFormOptions(Me) + End Sub + Private Sub PlaylistArrayForm_Load(sender As Object, e As EventArgs) Handles Me.Load + With MyDefs + .MyViewInitialize() + .AddOkCancelToolbar() + .EndLoaderOperations() + End With + End Sub + Private Sub PlaylistArrayForm_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown + If e.KeyCode = Keys.O And e.Control Then + Using f As New PlayListParserForm With {.DesignXML = DesignXML} + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then TXT_URLS.Text = f.PlayLists.ListToString(vbNewLine, EDP.ReturnValue) + End Using + e.Handled = True + End If + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOption.Designer.vb b/SCrawler.YouTube/Controls/VideoOption.Designer.vb new file mode 100644 index 0000000..15861b7 --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOption.Designer.vb @@ -0,0 +1,132 @@ +' 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.YouTube.Controls + + Partial Friend Class VideoOption : Inherits System.Windows.Forms.UserControl + + 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 TP_MAIN As System.Windows.Forms.TableLayoutPanel + Me.OPT_CHECKED = New System.Windows.Forms.RadioButton() + Me.LBL_DEFINITION_INFO = New System.Windows.Forms.Label() + Me.LBL_DEFINITION = New System.Windows.Forms.Label() + Me.LBL_CODECS = New System.Windows.Forms.Label() + Me.LBL_SIZE = New System.Windows.Forms.Label() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + TP_MAIN.SuspendLayout() + Me.SuspendLayout() + ' + 'TP_MAIN + ' + TP_MAIN.ColumnCount = 5 + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 36.0!)) + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 110.0!)) + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 58.0!)) + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 71.0!)) + TP_MAIN.Controls.Add(Me.OPT_CHECKED, 0, 0) + TP_MAIN.Controls.Add(Me.LBL_DEFINITION_INFO, 1, 0) + TP_MAIN.Controls.Add(Me.LBL_DEFINITION, 2, 0) + TP_MAIN.Controls.Add(Me.LBL_CODECS, 3, 0) + TP_MAIN.Controls.Add(Me.LBL_SIZE, 4, 0) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Margin = New System.Windows.Forms.Padding(0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 1 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Size = New System.Drawing.Size(459, 30) + TP_MAIN.TabIndex = 0 + ' + 'OPT_CHECKED + ' + Me.OPT_CHECKED.AutoSize = True + Me.OPT_CHECKED.CheckAlign = System.Drawing.ContentAlignment.MiddleCenter + Me.OPT_CHECKED.Dock = System.Windows.Forms.DockStyle.Fill + Me.OPT_CHECKED.Location = New System.Drawing.Point(3, 3) + Me.OPT_CHECKED.Name = "OPT_CHECKED" + Me.OPT_CHECKED.Size = New System.Drawing.Size(30, 24) + Me.OPT_CHECKED.TabIndex = 0 + Me.OPT_CHECKED.TabStop = True + Me.OPT_CHECKED.TextAlign = System.Drawing.ContentAlignment.MiddleCenter + Me.OPT_CHECKED.UseVisualStyleBackColor = True + ' + 'LBL_DEFINITION_INFO + ' + Me.LBL_DEFINITION_INFO.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_DEFINITION_INFO.Font = New System.Drawing.Font("Arial", 8.25!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_DEFINITION_INFO.Location = New System.Drawing.Point(39, 0) + Me.LBL_DEFINITION_INFO.Name = "LBL_DEFINITION_INFO" + Me.LBL_DEFINITION_INFO.Size = New System.Drawing.Size(104, 30) + Me.LBL_DEFINITION_INFO.TabIndex = 1 + Me.LBL_DEFINITION_INFO.Text = "Ultra High Definition" + Me.LBL_DEFINITION_INFO.TextAlign = System.Drawing.ContentAlignment.MiddleLeft + ' + 'LBL_DEFINITION + ' + Me.LBL_DEFINITION.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_DEFINITION.Font = New System.Drawing.Font("Arial", 9.0!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_DEFINITION.Location = New System.Drawing.Point(149, 0) + Me.LBL_DEFINITION.Name = "LBL_DEFINITION" + Me.LBL_DEFINITION.Size = New System.Drawing.Size(52, 30) + Me.LBL_DEFINITION.TabIndex = 2 + Me.LBL_DEFINITION.Text = "1080p" + Me.LBL_DEFINITION.TextAlign = System.Drawing.ContentAlignment.MiddleLeft + ' + 'LBL_CODECS + ' + Me.LBL_CODECS.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_CODECS.Font = New System.Drawing.Font("Arial", 8.25!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_CODECS.Location = New System.Drawing.Point(207, 0) + Me.LBL_CODECS.Name = "LBL_CODECS" + Me.LBL_CODECS.Size = New System.Drawing.Size(178, 30) + Me.LBL_CODECS.TabIndex = 3 + Me.LBL_CODECS.Text = "MKV - VP9 - Opus" + Me.LBL_CODECS.TextAlign = System.Drawing.ContentAlignment.MiddleRight + ' + 'LBL_SIZE + ' + Me.LBL_SIZE.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_SIZE.Font = New System.Drawing.Font("Arial", 8.25!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_SIZE.Location = New System.Drawing.Point(391, 0) + Me.LBL_SIZE.Name = "LBL_SIZE" + Me.LBL_SIZE.Size = New System.Drawing.Size(65, 30) + Me.LBL_SIZE.TabIndex = 4 + Me.LBL_SIZE.Text = "1 062 MB" + Me.LBL_SIZE.TextAlign = System.Drawing.ContentAlignment.MiddleRight + ' + 'VideoOption + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.Controls.Add(TP_MAIN) + Me.Name = "VideoOption" + Me.Size = New System.Drawing.Size(459, 30) + TP_MAIN.ResumeLayout(False) + TP_MAIN.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Private WithEvents OPT_CHECKED As RadioButton + Private WithEvents LBL_DEFINITION_INFO As Label + Private WithEvents LBL_DEFINITION As Label + Private WithEvents LBL_CODECS As Label + Private WithEvents LBL_SIZE As Label + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOption.resx b/SCrawler.YouTube/Controls/VideoOption.resx new file mode 100644 index 0000000..e5f5c30 --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOption.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOption.vb b/SCrawler.YouTube/Controls/VideoOption.vb new file mode 100644 index 0000000..a730200 --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOption.vb @@ -0,0 +1,87 @@ +' 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.ComponentModel +Imports SCrawler.API.YouTube.Base +Namespace API.YouTube.Controls + + Friend Class VideoOption : Implements ISupportInitialize + Friend Event CheckedChanged As EventHandler + Friend Property MyMedia As MediaObject + Friend Property Checked As Boolean + Get + Return OPT_CHECKED.Checked + End Get + Set(ByVal _Checked As Boolean) + OPT_CHECKED.Checked = _Checked + End Set + End Property + Friend Sub New() + InitializeComponent() + End Sub + Friend Sub New(ByVal m As MediaObject, Optional ByVal SelectedAudio As MediaObject = Nothing) + Me.New + Const d$ = " " & ChrW(183) & " " + MyMedia = m + If m.Type = Plugin.UserMediaTypes.Audio Then + If m.Bitrate >= 320 Then + LBL_DEFINITION_INFO.Text = "High Quality" + ElseIf m.Bitrate >= 190 Then + LBL_DEFINITION_INFO.Text = "Medium Quality" + Else + LBL_DEFINITION_INFO.Text = "Low Quality" + End If + LBL_DEFINITION.Text = $"{m.Bitrate}k" + LBL_CODECS.Text = $"{m.Extension} {d} {m.Codec} {d} {m.Bitrate}k" + Else + If m.Height >= 1440 Then + LBL_DEFINITION_INFO.Text = "Ultra High Definition" + ElseIf m.Height >= 720 Then + LBL_DEFINITION_INFO.Text = "High Definition" + ElseIf m.Height >= 480 Then + LBL_DEFINITION_INFO.Text = "Medium Definition" + ElseIf m.Height >= 360 Then + LBL_DEFINITION_INFO.Text = "Normal Definition" + Else + LBL_DEFINITION_INFO.Text = "Low Definition" + End If + LBL_DEFINITION.Text = $"{m.Height}p" + LBL_CODECS.Text = $"{m.Extension.StringToUpper}{d}{m.Codec.StringToUpper}{d}{m.FPS}fps{d}{m.Bitrate}k" + If Not SelectedAudio.ID.IsEmptyString Then LBL_CODECS.Text &= $" / {SelectedAudio.Extension}{d}{SelectedAudio.Codec}{d}{SelectedAudio.Bitrate}k" + End If + + Dim sv% = m.Size / 1024 + If sv >= 1000 Then + LBL_SIZE.Text = AConvert(Of String)(sv / 1024, VideoSizeProvider) + LBL_SIZE.Text &= " GB" + Else + LBL_SIZE.Text = AConvert(Of String)(sv, VideoSizeProvider) + LBL_SIZE.Text &= " MB" + End If + End Sub + Private Sub OPT_CHECKED_CheckedChanged(sender As Object, e As EventArgs) Handles OPT_CHECKED.CheckedChanged + RaiseEvent CheckedChanged(Me, e) + End Sub + Private Sub Labels_Click(sender As Object, e As EventArgs) Handles LBL_DEFINITION_INFO.Click, LBL_DEFINITION.Click, LBL_CODECS.Click, LBL_SIZE.Click + OPT_CHECKED.Checked = True + End Sub + Private Sub BindedControl_CheckedChanged(sender As Object, e As EventArgs) + If Not sender Is Nothing AndAlso Not sender Is Me AndAlso DirectCast(sender, VideoOption).Checked Then Checked = False + End Sub + Private Sub BeginInit() Implements ISupportInitialize.BeginInit + End Sub + Private Sub EndInit() Implements ISupportInitialize.EndInit + If Not Parent Is Nothing AndAlso Parent.Controls.Count > 0 Then + For Each cnt As Control In Parent.Controls + If TypeOf cnt Is VideoOption And Not cnt Is Me Then _ + AddHandler DirectCast(cnt, VideoOption).CheckedChanged, AddressOf BindedControl_CheckedChanged + Next + End If + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOptionsForm.Designer.vb b/SCrawler.YouTube/Controls/VideoOptionsForm.Designer.vb new file mode 100644 index 0000000..633c461 --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOptionsForm.Designer.vb @@ -0,0 +1,749 @@ +' 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.YouTube.Controls + + Partial Friend Class VideoOptionsForm : 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() + Me.components = New System.ComponentModel.Container() + Dim TP_HEADER As System.Windows.Forms.TableLayoutPanel + Dim TP_HEADER_INFO As System.Windows.Forms.TableLayoutPanel + Dim ICON_CLOCK As System.Windows.Forms.PictureBox + Dim ICON_LINK As System.Windows.Forms.PictureBox + Dim TP_FOOTER As System.Windows.Forms.TableLayoutPanel + Dim TP_DESTINATION As System.Windows.Forms.TableLayoutPanel + Dim TP_OK_CANCEL As System.Windows.Forms.TableLayoutPanel + Dim LB_SEP_1 As System.Windows.Forms.Label + Dim LB_SEP_2 As System.Windows.Forms.Label + Dim TP_WHAT As System.Windows.Forms.TableLayoutPanel + Dim LBL_WHAT As System.Windows.Forms.Label + Dim LBL_FORMAT As System.Windows.Forms.Label + Dim LBL_SUBS_FORMAT As System.Windows.Forms.Label + Dim TT_MAIN As System.Windows.Forms.ToolTip + Dim ActionButton1 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(VideoOptionsForm)) + Dim ActionButton2 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton3 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton4 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton5 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton6 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton7 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton8 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton9 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Me.ICON_VIDEO = New System.Windows.Forms.PictureBox() + Me.LBL_TITLE = New System.Windows.Forms.Label() + Me.TP_HEADER_INFO_2 = New System.Windows.Forms.TableLayoutPanel() + Me.LBL_TIME = New System.Windows.Forms.Label() + Me.LBL_URL = New System.Windows.Forms.LinkLabel() + Me.TXT_FILE = New System.Windows.Forms.TextBox() + Me.BTT_BROWSE = New System.Windows.Forms.Button() + Me.BTT_DOWN = New System.Windows.Forms.Button() + Me.BTT_CANCEL = New System.Windows.Forms.Button() + Me.OPT_VIDEO = New System.Windows.Forms.RadioButton() + Me.OPT_AUDIO = New System.Windows.Forms.RadioButton() + Me.LBL_AUDIO_CODEC = New System.Windows.Forms.Label() + Me.TP_HEADER_BASE = New System.Windows.Forms.TableLayoutPanel() + Me.TP_SUBS = New System.Windows.Forms.TableLayoutPanel() + Me.TXT_SUBS = New PersonalUtilities.Forms.Controls.TextBoxExtended() + Me.CMB_SUBS_FORMAT = New System.Windows.Forms.ComboBox() + Me.TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + Me.TP_OPTIONS = New System.Windows.Forms.TableLayoutPanel() + Me.CMB_FORMAT = New System.Windows.Forms.ComboBox() + Me.CMB_AUDIO_CODEC = New System.Windows.Forms.ComboBox() + Me.NUM_RES = New System.Windows.Forms.NumericUpDown() + Me.TP_CONTROLS = New System.Windows.Forms.TableLayoutPanel() + Me.TXT_SUBS_ADDIT = New PersonalUtilities.Forms.Controls.TextBoxExtended() + Me.TXT_EXTRA_AUDIO_FORMATS = New PersonalUtilities.Forms.Controls.TextBoxExtended() + TP_HEADER = New System.Windows.Forms.TableLayoutPanel() + TP_HEADER_INFO = New System.Windows.Forms.TableLayoutPanel() + ICON_CLOCK = New System.Windows.Forms.PictureBox() + ICON_LINK = New System.Windows.Forms.PictureBox() + TP_FOOTER = New System.Windows.Forms.TableLayoutPanel() + TP_DESTINATION = New System.Windows.Forms.TableLayoutPanel() + TP_OK_CANCEL = New System.Windows.Forms.TableLayoutPanel() + LB_SEP_1 = New System.Windows.Forms.Label() + LB_SEP_2 = New System.Windows.Forms.Label() + TP_WHAT = New System.Windows.Forms.TableLayoutPanel() + LBL_WHAT = New System.Windows.Forms.Label() + LBL_FORMAT = New System.Windows.Forms.Label() + LBL_SUBS_FORMAT = New System.Windows.Forms.Label() + TT_MAIN = New System.Windows.Forms.ToolTip(Me.components) + TP_HEADER.SuspendLayout() + CType(Me.ICON_VIDEO, System.ComponentModel.ISupportInitialize).BeginInit() + TP_HEADER_INFO.SuspendLayout() + Me.TP_HEADER_INFO_2.SuspendLayout() + CType(ICON_CLOCK, System.ComponentModel.ISupportInitialize).BeginInit() + CType(ICON_LINK, System.ComponentModel.ISupportInitialize).BeginInit() + TP_FOOTER.SuspendLayout() + TP_DESTINATION.SuspendLayout() + TP_OK_CANCEL.SuspendLayout() + TP_WHAT.SuspendLayout() + Me.TP_HEADER_BASE.SuspendLayout() + Me.TP_SUBS.SuspendLayout() + CType(Me.TXT_SUBS, System.ComponentModel.ISupportInitialize).BeginInit() + Me.TP_MAIN.SuspendLayout() + Me.TP_OPTIONS.SuspendLayout() + CType(Me.NUM_RES, System.ComponentModel.ISupportInitialize).BeginInit() + CType(Me.TXT_SUBS_ADDIT, System.ComponentModel.ISupportInitialize).BeginInit() + CType(Me.TXT_EXTRA_AUDIO_FORMATS, System.ComponentModel.ISupportInitialize).BeginInit() + Me.SuspendLayout() + ' + 'TP_HEADER + ' + TP_HEADER.BackColor = System.Drawing.SystemColors.Window + TP_HEADER.ColumnCount = 2 + TP_HEADER.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 130.0!)) + TP_HEADER.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_HEADER.Controls.Add(Me.ICON_VIDEO, 0, 0) + TP_HEADER.Controls.Add(TP_HEADER_INFO, 1, 0) + TP_HEADER.Dock = System.Windows.Forms.DockStyle.Fill + TP_HEADER.Location = New System.Drawing.Point(1, 1) + TP_HEADER.Margin = New System.Windows.Forms.Padding(0) + TP_HEADER.Name = "TP_HEADER" + TP_HEADER.RowCount = 1 + TP_HEADER.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_HEADER.Size = New System.Drawing.Size(599, 63) + TP_HEADER.TabIndex = 0 + ' + 'ICON_VIDEO + ' + Me.ICON_VIDEO.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom + Me.ICON_VIDEO.Dock = System.Windows.Forms.DockStyle.Fill + Me.ICON_VIDEO.Location = New System.Drawing.Point(1, 1) + Me.ICON_VIDEO.Margin = New System.Windows.Forms.Padding(1) + Me.ICON_VIDEO.Name = "ICON_VIDEO" + Me.ICON_VIDEO.Size = New System.Drawing.Size(128, 61) + Me.ICON_VIDEO.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom + Me.ICON_VIDEO.TabIndex = 0 + Me.ICON_VIDEO.TabStop = False + ' + 'TP_HEADER_INFO + ' + TP_HEADER_INFO.ColumnCount = 1 + TP_HEADER_INFO.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_HEADER_INFO.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_HEADER_INFO.Controls.Add(Me.LBL_TITLE, 0, 0) + TP_HEADER_INFO.Controls.Add(Me.TP_HEADER_INFO_2, 0, 1) + TP_HEADER_INFO.Dock = System.Windows.Forms.DockStyle.Fill + TP_HEADER_INFO.Location = New System.Drawing.Point(130, 0) + TP_HEADER_INFO.Margin = New System.Windows.Forms.Padding(0) + TP_HEADER_INFO.Name = "TP_HEADER_INFO" + TP_HEADER_INFO.RowCount = 2 + TP_HEADER_INFO.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_HEADER_INFO.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_HEADER_INFO.Size = New System.Drawing.Size(469, 63) + TP_HEADER_INFO.TabIndex = 0 + ' + 'LBL_TITLE + ' + Me.LBL_TITLE.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_TITLE.Font = New System.Drawing.Font("Arial", 9.0!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_TITLE.Location = New System.Drawing.Point(3, 0) + Me.LBL_TITLE.Name = "LBL_TITLE" + Me.LBL_TITLE.Size = New System.Drawing.Size(463, 31) + Me.LBL_TITLE.TabIndex = 0 + Me.LBL_TITLE.Text = "Video title" + Me.LBL_TITLE.TextAlign = System.Drawing.ContentAlignment.MiddleLeft + ' + 'TP_HEADER_INFO_2 + ' + Me.TP_HEADER_INFO_2.ColumnCount = 4 + Me.TP_HEADER_INFO_2.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + Me.TP_HEADER_INFO_2.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 62.0!)) + Me.TP_HEADER_INFO_2.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + Me.TP_HEADER_INFO_2.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_HEADER_INFO_2.Controls.Add(ICON_CLOCK, 0, 0) + Me.TP_HEADER_INFO_2.Controls.Add(Me.LBL_TIME, 1, 0) + Me.TP_HEADER_INFO_2.Controls.Add(ICON_LINK, 2, 0) + Me.TP_HEADER_INFO_2.Controls.Add(Me.LBL_URL, 3, 0) + Me.TP_HEADER_INFO_2.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_HEADER_INFO_2.Location = New System.Drawing.Point(0, 31) + Me.TP_HEADER_INFO_2.Margin = New System.Windows.Forms.Padding(0) + Me.TP_HEADER_INFO_2.Name = "TP_HEADER_INFO_2" + Me.TP_HEADER_INFO_2.RowCount = 1 + Me.TP_HEADER_INFO_2.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_HEADER_INFO_2.Size = New System.Drawing.Size(469, 32) + Me.TP_HEADER_INFO_2.TabIndex = 1 + ' + 'ICON_CLOCK + ' + ICON_CLOCK.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom + ICON_CLOCK.Dock = System.Windows.Forms.DockStyle.Fill + ICON_CLOCK.Image = Global.SCrawler.My.Resources.Resources.ClockPic_16 + ICON_CLOCK.Location = New System.Drawing.Point(3, 3) + ICON_CLOCK.Name = "ICON_CLOCK" + ICON_CLOCK.Size = New System.Drawing.Size(19, 26) + ICON_CLOCK.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom + ICON_CLOCK.TabIndex = 0 + ICON_CLOCK.TabStop = False + ' + 'LBL_TIME + ' + Me.LBL_TIME.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_TIME.Font = New System.Drawing.Font("Arial", 9.0!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_TIME.ForeColor = System.Drawing.SystemColors.ControlDarkDark + Me.LBL_TIME.Location = New System.Drawing.Point(28, 0) + Me.LBL_TIME.Name = "LBL_TIME" + Me.LBL_TIME.Size = New System.Drawing.Size(56, 32) + Me.LBL_TIME.TabIndex = 0 + Me.LBL_TIME.Text = "00:00:00" + Me.LBL_TIME.TextAlign = System.Drawing.ContentAlignment.MiddleCenter + ' + 'ICON_LINK + ' + ICON_LINK.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom + ICON_LINK.Dock = System.Windows.Forms.DockStyle.Fill + ICON_LINK.Image = Global.SCrawler.My.Resources.Resources.LinkPic_32 + ICON_LINK.Location = New System.Drawing.Point(90, 3) + ICON_LINK.Name = "ICON_LINK" + ICON_LINK.Size = New System.Drawing.Size(19, 26) + ICON_LINK.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom + ICON_LINK.TabIndex = 2 + ICON_LINK.TabStop = False + ' + 'LBL_URL + ' + Me.LBL_URL.AutoSize = True + Me.LBL_URL.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_URL.Font = New System.Drawing.Font("Arial", 9.0!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_URL.LinkColor = System.Drawing.Color.FromArgb(CType(CType(0, Byte), Integer), CType(CType(0, Byte), Integer), CType(CType(192, Byte), Integer)) + Me.LBL_URL.Location = New System.Drawing.Point(115, 0) + Me.LBL_URL.Name = "LBL_URL" + Me.LBL_URL.Size = New System.Drawing.Size(351, 32) + Me.LBL_URL.TabIndex = 1 + Me.LBL_URL.TabStop = True + Me.LBL_URL.Text = "https://www.youtube.com/watch?v=abcdefghijk" + Me.LBL_URL.TextAlign = System.Drawing.ContentAlignment.MiddleLeft + ' + 'TP_FOOTER + ' + TP_FOOTER.ColumnCount = 1 + TP_FOOTER.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_FOOTER.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_FOOTER.Controls.Add(TP_DESTINATION, 0, 0) + TP_FOOTER.Controls.Add(TP_OK_CANCEL, 0, 1) + TP_FOOTER.Dock = System.Windows.Forms.DockStyle.Fill + TP_FOOTER.Location = New System.Drawing.Point(6, 215) + TP_FOOTER.Margin = New System.Windows.Forms.Padding(6, 3, 6, 3) + TP_FOOTER.Name = "TP_FOOTER" + TP_FOOTER.RowCount = 2 + TP_FOOTER.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_FOOTER.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_FOOTER.Size = New System.Drawing.Size(589, 52) + TP_FOOTER.TabIndex = 5 + ' + 'TP_DESTINATION + ' + TP_DESTINATION.ColumnCount = 2 + TP_DESTINATION.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_DESTINATION.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + TP_DESTINATION.Controls.Add(Me.TXT_FILE, 0, 0) + TP_DESTINATION.Controls.Add(Me.BTT_BROWSE, 1, 0) + TP_DESTINATION.Dock = System.Windows.Forms.DockStyle.Fill + TP_DESTINATION.Location = New System.Drawing.Point(0, 0) + TP_DESTINATION.Margin = New System.Windows.Forms.Padding(0) + TP_DESTINATION.Name = "TP_DESTINATION" + TP_DESTINATION.RowCount = 1 + TP_DESTINATION.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_DESTINATION.Size = New System.Drawing.Size(589, 26) + TP_DESTINATION.TabIndex = 0 + ' + 'TXT_FILE + ' + Me.TXT_FILE.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_FILE.Location = New System.Drawing.Point(3, 3) + Me.TXT_FILE.Name = "TXT_FILE" + Me.TXT_FILE.Size = New System.Drawing.Size(503, 20) + Me.TXT_FILE.TabIndex = 0 + Me.TXT_FILE.WordWrap = False + ' + 'BTT_BROWSE + ' + Me.BTT_BROWSE.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_BROWSE.Location = New System.Drawing.Point(512, 2) + Me.BTT_BROWSE.Margin = New System.Windows.Forms.Padding(3, 2, 3, 2) + Me.BTT_BROWSE.Name = "BTT_BROWSE" + Me.BTT_BROWSE.Size = New System.Drawing.Size(74, 22) + Me.BTT_BROWSE.TabIndex = 1 + Me.BTT_BROWSE.Text = "Browse" + TT_MAIN.SetToolTip(Me.BTT_BROWSE, "Choose an output file") + Me.BTT_BROWSE.UseVisualStyleBackColor = True + ' + 'TP_OK_CANCEL + ' + TP_OK_CANCEL.ColumnCount = 3 + TP_OK_CANCEL.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_OK_CANCEL.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + TP_OK_CANCEL.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + TP_OK_CANCEL.Controls.Add(Me.BTT_DOWN, 1, 0) + TP_OK_CANCEL.Controls.Add(Me.BTT_CANCEL, 2, 0) + TP_OK_CANCEL.Dock = System.Windows.Forms.DockStyle.Fill + TP_OK_CANCEL.Location = New System.Drawing.Point(0, 26) + TP_OK_CANCEL.Margin = New System.Windows.Forms.Padding(0) + TP_OK_CANCEL.Name = "TP_OK_CANCEL" + TP_OK_CANCEL.RowCount = 1 + TP_OK_CANCEL.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_OK_CANCEL.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 26.0!)) + TP_OK_CANCEL.Size = New System.Drawing.Size(589, 26) + TP_OK_CANCEL.TabIndex = 1 + ' + 'BTT_DOWN + ' + Me.BTT_DOWN.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_DOWN.Location = New System.Drawing.Point(432, 2) + Me.BTT_DOWN.Margin = New System.Windows.Forms.Padding(3, 2, 3, 2) + Me.BTT_DOWN.Name = "BTT_DOWN" + Me.BTT_DOWN.Size = New System.Drawing.Size(74, 22) + Me.BTT_DOWN.TabIndex = 0 + Me.BTT_DOWN.Text = "Download" + Me.BTT_DOWN.UseVisualStyleBackColor = True + ' + 'BTT_CANCEL + ' + Me.BTT_CANCEL.DialogResult = System.Windows.Forms.DialogResult.Cancel + Me.BTT_CANCEL.Dock = System.Windows.Forms.DockStyle.Fill + Me.BTT_CANCEL.Location = New System.Drawing.Point(512, 2) + Me.BTT_CANCEL.Margin = New System.Windows.Forms.Padding(3, 2, 3, 2) + Me.BTT_CANCEL.Name = "BTT_CANCEL" + Me.BTT_CANCEL.Size = New System.Drawing.Size(74, 22) + Me.BTT_CANCEL.TabIndex = 1 + Me.BTT_CANCEL.Text = "Cancel" + Me.BTT_CANCEL.UseVisualStyleBackColor = True + ' + 'LB_SEP_1 + ' + LB_SEP_1.Anchor = CType((System.Windows.Forms.AnchorStyles.Left Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + LB_SEP_1.BackColor = System.Drawing.SystemColors.ControlDark + LB_SEP_1.Location = New System.Drawing.Point(6, 179) + LB_SEP_1.Margin = New System.Windows.Forms.Padding(6, 0, 6, 0) + LB_SEP_1.Name = "LB_SEP_1" + LB_SEP_1.Size = New System.Drawing.Size(589, 1) + LB_SEP_1.TabIndex = 3 + ' + 'LB_SEP_2 + ' + LB_SEP_2.Anchor = CType((System.Windows.Forms.AnchorStyles.Left Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + LB_SEP_2.BackColor = System.Drawing.SystemColors.ControlDark + LB_SEP_2.Location = New System.Drawing.Point(6, 209) + LB_SEP_2.Margin = New System.Windows.Forms.Padding(6, 0, 6, 0) + LB_SEP_2.Name = "LB_SEP_2" + LB_SEP_2.Size = New System.Drawing.Size(589, 1) + LB_SEP_2.TabIndex = 5 + ' + 'TP_WHAT + ' + TP_WHAT.ColumnCount = 3 + TP_WHAT.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 65.0!)) + TP_WHAT.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_WHAT.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + TP_WHAT.Controls.Add(LBL_WHAT, 0, 0) + TP_WHAT.Controls.Add(Me.OPT_VIDEO, 1, 0) + TP_WHAT.Controls.Add(Me.OPT_AUDIO, 2, 0) + TP_WHAT.Dock = System.Windows.Forms.DockStyle.Fill + TP_WHAT.Location = New System.Drawing.Point(0, 0) + TP_WHAT.Margin = New System.Windows.Forms.Padding(0) + TP_WHAT.Name = "TP_WHAT" + TP_WHAT.RowCount = 1 + TP_WHAT.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_WHAT.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + TP_WHAT.Size = New System.Drawing.Size(189, 28) + TP_WHAT.TabIndex = 0 + ' + 'LBL_WHAT + ' + LBL_WHAT.AutoSize = True + LBL_WHAT.Dock = System.Windows.Forms.DockStyle.Fill + LBL_WHAT.Location = New System.Drawing.Point(3, 0) + LBL_WHAT.Name = "LBL_WHAT" + LBL_WHAT.Size = New System.Drawing.Size(59, 28) + LBL_WHAT.TabIndex = 4 + LBL_WHAT.Text = "Download" + LBL_WHAT.TextAlign = System.Drawing.ContentAlignment.MiddleRight + ' + 'OPT_VIDEO + ' + Me.OPT_VIDEO.AutoSize = True + Me.OPT_VIDEO.Dock = System.Windows.Forms.DockStyle.Fill + Me.OPT_VIDEO.Location = New System.Drawing.Point(68, 3) + Me.OPT_VIDEO.Name = "OPT_VIDEO" + Me.OPT_VIDEO.Size = New System.Drawing.Size(56, 22) + Me.OPT_VIDEO.TabIndex = 0 + Me.OPT_VIDEO.TabStop = True + Me.OPT_VIDEO.Text = "Video" + Me.OPT_VIDEO.UseVisualStyleBackColor = True + ' + 'OPT_AUDIO + ' + Me.OPT_AUDIO.AutoSize = True + Me.OPT_AUDIO.Dock = System.Windows.Forms.DockStyle.Fill + Me.OPT_AUDIO.Location = New System.Drawing.Point(130, 3) + Me.OPT_AUDIO.Name = "OPT_AUDIO" + Me.OPT_AUDIO.Size = New System.Drawing.Size(56, 22) + Me.OPT_AUDIO.TabIndex = 1 + Me.OPT_AUDIO.TabStop = True + Me.OPT_AUDIO.Text = "Audio" + Me.OPT_AUDIO.UseVisualStyleBackColor = True + ' + 'LBL_FORMAT + ' + LBL_FORMAT.Dock = System.Windows.Forms.DockStyle.Fill + LBL_FORMAT.Location = New System.Drawing.Point(192, 0) + LBL_FORMAT.Name = "LBL_FORMAT" + LBL_FORMAT.Size = New System.Drawing.Size(74, 28) + LBL_FORMAT.TabIndex = 4 + LBL_FORMAT.Text = "Format:" + LBL_FORMAT.TextAlign = System.Drawing.ContentAlignment.MiddleRight + TT_MAIN.SetToolTip(LBL_FORMAT, "Output Video Format") + ' + 'LBL_SUBS_FORMAT + ' + LBL_SUBS_FORMAT.AutoSize = True + LBL_SUBS_FORMAT.Dock = System.Windows.Forms.DockStyle.Fill + LBL_SUBS_FORMAT.Location = New System.Drawing.Point(432, 0) + LBL_SUBS_FORMAT.Name = "LBL_SUBS_FORMAT" + LBL_SUBS_FORMAT.Size = New System.Drawing.Size(74, 28) + LBL_SUBS_FORMAT.TabIndex = 2 + LBL_SUBS_FORMAT.Text = "Format:" + LBL_SUBS_FORMAT.TextAlign = System.Drawing.ContentAlignment.MiddleRight + TT_MAIN.SetToolTip(LBL_SUBS_FORMAT, "Output Subtitles Format") + ' + 'LBL_AUDIO_CODEC + ' + Me.LBL_AUDIO_CODEC.AutoSize = True + Me.LBL_AUDIO_CODEC.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_AUDIO_CODEC.Location = New System.Drawing.Point(432, 0) + Me.LBL_AUDIO_CODEC.Name = "LBL_AUDIO_CODEC" + Me.LBL_AUDIO_CODEC.Size = New System.Drawing.Size(74, 28) + Me.LBL_AUDIO_CODEC.TabIndex = 5 + Me.LBL_AUDIO_CODEC.Text = "Audio Codec" + Me.LBL_AUDIO_CODEC.TextAlign = System.Drawing.ContentAlignment.MiddleRight + TT_MAIN.SetToolTip(Me.LBL_AUDIO_CODEC, "Output Audio Codec") + ' + 'TP_HEADER_BASE + ' + Me.TP_HEADER_BASE.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.[Single] + Me.TP_HEADER_BASE.ColumnCount = 1 + Me.TP_HEADER_BASE.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_HEADER_BASE.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + Me.TP_HEADER_BASE.Controls.Add(TP_HEADER, 0, 0) + Me.TP_HEADER_BASE.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_HEADER_BASE.Location = New System.Drawing.Point(0, 0) + Me.TP_HEADER_BASE.Margin = New System.Windows.Forms.Padding(0) + Me.TP_HEADER_BASE.Name = "TP_HEADER_BASE" + Me.TP_HEADER_BASE.RowCount = 1 + Me.TP_HEADER_BASE.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_HEADER_BASE.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 64.0!)) + Me.TP_HEADER_BASE.Size = New System.Drawing.Size(601, 65) + Me.TP_HEADER_BASE.TabIndex = 6 + ' + 'TP_SUBS + ' + Me.TP_SUBS.ColumnCount = 3 + Me.TP_SUBS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_SUBS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_SUBS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_SUBS.Controls.Add(Me.TXT_SUBS, 0, 0) + Me.TP_SUBS.Controls.Add(LBL_SUBS_FORMAT, 1, 0) + Me.TP_SUBS.Controls.Add(Me.CMB_SUBS_FORMAT, 2, 0) + Me.TP_SUBS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_SUBS.Location = New System.Drawing.Point(6, 93) + Me.TP_SUBS.Margin = New System.Windows.Forms.Padding(6, 0, 6, 0) + Me.TP_SUBS.Name = "TP_SUBS" + Me.TP_SUBS.RowCount = 1 + Me.TP_SUBS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_SUBS.Size = New System.Drawing.Size(589, 28) + Me.TP_SUBS.TabIndex = 2 + ' + 'TXT_SUBS + ' + ActionButton1.BackgroundImage = CType(resources.GetObject("ActionButton1.BackgroundImage"), System.Drawing.Image) + ActionButton1.Name = "Open" + ActionButton1.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton1.ToolTipText = "Choose subtitles" + ActionButton2.BackgroundImage = CType(resources.GetObject("ActionButton2.BackgroundImage"), System.Drawing.Image) + ActionButton2.Name = "Refresh" + ActionButton2.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Refresh + ActionButton2.ToolTipText = "Reset subtitles to initial selected" + ActionButton3.BackgroundImage = CType(resources.GetObject("ActionButton3.BackgroundImage"), System.Drawing.Image) + ActionButton3.Name = "Clear" + ActionButton3.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + ActionButton3.ToolTipText = "Clear subtitles selection (don't download subtitles)" + Me.TXT_SUBS.Buttons.Add(ActionButton1) + Me.TXT_SUBS.Buttons.Add(ActionButton2) + Me.TXT_SUBS.Buttons.Add(ActionButton3) + Me.TXT_SUBS.CaptionText = "Subtitles" + Me.TXT_SUBS.CaptionToolTipEnabled = True + Me.TXT_SUBS.CaptionToolTipText = "The selected subtitles will also be downloaded" + Me.TXT_SUBS.CaptionWidth = 60.0R + Me.TXT_SUBS.ClearTextByButtonClear = False + Me.TXT_SUBS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_SUBS.Location = New System.Drawing.Point(3, 3) + Me.TXT_SUBS.Name = "TXT_SUBS" + Me.TXT_SUBS.Size = New System.Drawing.Size(423, 22) + Me.TXT_SUBS.TabIndex = 0 + Me.TXT_SUBS.TextBoxReadOnly = True + ' + 'CMB_SUBS_FORMAT + ' + Me.CMB_SUBS_FORMAT.Dock = System.Windows.Forms.DockStyle.Fill + Me.CMB_SUBS_FORMAT.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList + Me.CMB_SUBS_FORMAT.FormattingEnabled = True + Me.CMB_SUBS_FORMAT.Location = New System.Drawing.Point(512, 3) + Me.CMB_SUBS_FORMAT.Name = "CMB_SUBS_FORMAT" + Me.CMB_SUBS_FORMAT.Size = New System.Drawing.Size(74, 21) + Me.CMB_SUBS_FORMAT.TabIndex = 1 + ' + 'TP_MAIN + ' + Me.TP_MAIN.ColumnCount = 1 + Me.TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_MAIN.Controls.Add(Me.TP_HEADER_BASE, 0, 0) + Me.TP_MAIN.Controls.Add(TP_FOOTER, 0, 8) + Me.TP_MAIN.Controls.Add(Me.TP_OPTIONS, 0, 1) + Me.TP_MAIN.Controls.Add(Me.TP_CONTROLS, 0, 6) + Me.TP_MAIN.Controls.Add(LB_SEP_1, 0, 5) + Me.TP_MAIN.Controls.Add(LB_SEP_2, 0, 7) + Me.TP_MAIN.Controls.Add(Me.TP_SUBS, 0, 2) + Me.TP_MAIN.Controls.Add(Me.TXT_SUBS_ADDIT, 0, 3) + Me.TP_MAIN.Controls.Add(Me.TXT_EXTRA_AUDIO_FORMATS, 0, 4) + Me.TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_MAIN.Location = New System.Drawing.Point(0, 0) + Me.TP_MAIN.Name = "TP_MAIN" + Me.TP_MAIN.RowCount = 10 + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 65.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 5.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 25.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 5.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle()) + Me.TP_MAIN.Size = New System.Drawing.Size(601, 271) + Me.TP_MAIN.TabIndex = 0 + ' + 'TP_OPTIONS + ' + Me.TP_OPTIONS.ColumnCount = 6 + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_OPTIONS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 80.0!)) + Me.TP_OPTIONS.Controls.Add(LBL_FORMAT, 1, 0) + Me.TP_OPTIONS.Controls.Add(TP_WHAT, 0, 0) + Me.TP_OPTIONS.Controls.Add(Me.CMB_FORMAT, 2, 0) + Me.TP_OPTIONS.Controls.Add(Me.LBL_AUDIO_CODEC, 4, 0) + Me.TP_OPTIONS.Controls.Add(Me.CMB_AUDIO_CODEC, 5, 0) + Me.TP_OPTIONS.Controls.Add(Me.NUM_RES, 3, 0) + Me.TP_OPTIONS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_OPTIONS.Location = New System.Drawing.Point(6, 65) + Me.TP_OPTIONS.Margin = New System.Windows.Forms.Padding(6, 0, 6, 0) + Me.TP_OPTIONS.Name = "TP_OPTIONS" + Me.TP_OPTIONS.RowCount = 1 + Me.TP_OPTIONS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_OPTIONS.Size = New System.Drawing.Size(589, 28) + Me.TP_OPTIONS.TabIndex = 1 + ' + 'CMB_FORMAT + ' + Me.CMB_FORMAT.Dock = System.Windows.Forms.DockStyle.Fill + Me.CMB_FORMAT.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList + Me.CMB_FORMAT.FormattingEnabled = True + Me.CMB_FORMAT.Location = New System.Drawing.Point(272, 3) + Me.CMB_FORMAT.Name = "CMB_FORMAT" + Me.CMB_FORMAT.Size = New System.Drawing.Size(74, 21) + Me.CMB_FORMAT.TabIndex = 1 + ' + 'CMB_AUDIO_CODEC + ' + Me.CMB_AUDIO_CODEC.Dock = System.Windows.Forms.DockStyle.Fill + Me.CMB_AUDIO_CODEC.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList + Me.CMB_AUDIO_CODEC.FormattingEnabled = True + Me.CMB_AUDIO_CODEC.Location = New System.Drawing.Point(512, 3) + Me.CMB_AUDIO_CODEC.Name = "CMB_AUDIO_CODEC" + Me.CMB_AUDIO_CODEC.Size = New System.Drawing.Size(74, 21) + Me.CMB_AUDIO_CODEC.TabIndex = 3 + ' + 'NUM_RES + ' + Me.NUM_RES.Dock = System.Windows.Forms.DockStyle.Fill + Me.NUM_RES.Location = New System.Drawing.Point(352, 3) + Me.NUM_RES.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) + Me.NUM_RES.Minimum = New Decimal(New Integer() {1, 0, 0, -2147483648}) + Me.NUM_RES.Name = "NUM_RES" + Me.NUM_RES.Size = New System.Drawing.Size(74, 20) + Me.NUM_RES.TabIndex = 2 + Me.NUM_RES.TextAlign = System.Windows.Forms.HorizontalAlignment.Center + Me.NUM_RES.Value = New Decimal(New Integer() {1080, 0, 0, 0}) + ' + 'TP_CONTROLS + ' + Me.TP_CONTROLS.ColumnCount = 1 + Me.TP_CONTROLS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_CONTROLS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_CONTROLS.Location = New System.Drawing.Point(3, 182) + Me.TP_CONTROLS.Margin = New System.Windows.Forms.Padding(3, 0, 3, 0) + Me.TP_CONTROLS.Name = "TP_CONTROLS" + Me.TP_CONTROLS.RowCount = 1 + Me.TP_CONTROLS.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_CONTROLS.Size = New System.Drawing.Size(595, 25) + Me.TP_CONTROLS.TabIndex = 0 + ' + 'TXT_SUBS_ADDIT + ' + ActionButton4.BackgroundImage = CType(resources.GetObject("ActionButton4.BackgroundImage"), System.Drawing.Image) + ActionButton4.Enabled = False + ActionButton4.Name = "Open" + ActionButton4.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton4.ToolTipText = "Choose additional formats" + ActionButton5.BackgroundImage = CType(resources.GetObject("ActionButton5.BackgroundImage"), System.Drawing.Image) + ActionButton5.Enabled = False + ActionButton5.Name = "Refresh" + ActionButton5.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Refresh + ActionButton5.ToolTipText = "Fill in additional formats from the defaults" + ActionButton6.BackgroundImage = CType(resources.GetObject("ActionButton6.BackgroundImage"), System.Drawing.Image) + ActionButton6.Enabled = False + ActionButton6.Name = "Clear" + ActionButton6.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + ActionButton6.ToolTipText = "Remove all additional formats" + Me.TXT_SUBS_ADDIT.Buttons.Add(ActionButton4) + Me.TXT_SUBS_ADDIT.Buttons.Add(ActionButton5) + Me.TXT_SUBS_ADDIT.Buttons.Add(ActionButton6) + Me.TXT_SUBS_ADDIT.CaptionMode = PersonalUtilities.Forms.Controls.Base.ICaptionControl.Modes.CheckBox + Me.TXT_SUBS_ADDIT.CaptionText = "Additional subtitle formats" + Me.TXT_SUBS_ADDIT.CaptionToolTipEnabled = True + Me.TXT_SUBS_ADDIT.CaptionToolTipText = "Subtitles will be downloaded in 'SRT' format and converted to additional formats" + Me.TXT_SUBS_ADDIT.CaptionWidth = 150.0R + Me.TXT_SUBS_ADDIT.ClearTextByButtonClear = False + Me.TXT_SUBS_ADDIT.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_SUBS_ADDIT.Location = New System.Drawing.Point(6, 124) + Me.TXT_SUBS_ADDIT.Margin = New System.Windows.Forms.Padding(6, 3, 6, 3) + Me.TXT_SUBS_ADDIT.Name = "TXT_SUBS_ADDIT" + Me.TXT_SUBS_ADDIT.Size = New System.Drawing.Size(589, 22) + Me.TXT_SUBS_ADDIT.TabIndex = 3 + Me.TXT_SUBS_ADDIT.Tag = "s" + Me.TXT_SUBS_ADDIT.TextBoxReadOnly = True + ' + 'TXT_EXTRA_AUDIO_FORMATS + ' + ActionButton7.BackgroundImage = CType(resources.GetObject("ActionButton7.BackgroundImage"), System.Drawing.Image) + ActionButton7.Enabled = False + ActionButton7.Name = "Open" + ActionButton7.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Open + ActionButton7.ToolTipText = "Choose additional formats" + ActionButton8.BackgroundImage = CType(resources.GetObject("ActionButton8.BackgroundImage"), System.Drawing.Image) + ActionButton8.Enabled = False + ActionButton8.Name = "Refresh" + ActionButton8.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Refresh + ActionButton8.ToolTipText = "Fill in additional formats from the defaults" + ActionButton9.BackgroundImage = CType(resources.GetObject("ActionButton9.BackgroundImage"), System.Drawing.Image) + ActionButton9.Enabled = False + ActionButton9.Name = "Clear" + ActionButton9.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + ActionButton9.ToolTipText = "Choose additional formats" + Me.TXT_EXTRA_AUDIO_FORMATS.Buttons.Add(ActionButton7) + Me.TXT_EXTRA_AUDIO_FORMATS.Buttons.Add(ActionButton8) + Me.TXT_EXTRA_AUDIO_FORMATS.Buttons.Add(ActionButton9) + Me.TXT_EXTRA_AUDIO_FORMATS.CaptionMode = PersonalUtilities.Forms.Controls.Base.ICaptionControl.Modes.CheckBox + Me.TXT_EXTRA_AUDIO_FORMATS.CaptionText = "Additional audio formats" + Me.TXT_EXTRA_AUDIO_FORMATS.CaptionToolTipEnabled = True + Me.TXT_EXTRA_AUDIO_FORMATS.CaptionWidth = 150.0R + Me.TXT_EXTRA_AUDIO_FORMATS.ClearTextByButtonClear = False + Me.TXT_EXTRA_AUDIO_FORMATS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_EXTRA_AUDIO_FORMATS.Location = New System.Drawing.Point(6, 152) + Me.TXT_EXTRA_AUDIO_FORMATS.Margin = New System.Windows.Forms.Padding(6, 3, 6, 3) + Me.TXT_EXTRA_AUDIO_FORMATS.Name = "TXT_EXTRA_AUDIO_FORMATS" + Me.TXT_EXTRA_AUDIO_FORMATS.Size = New System.Drawing.Size(589, 22) + Me.TXT_EXTRA_AUDIO_FORMATS.TabIndex = 4 + Me.TXT_EXTRA_AUDIO_FORMATS.Tag = "a" + Me.TXT_EXTRA_AUDIO_FORMATS.TextBoxReadOnly = True + ' + 'VideoOptionsForm + ' + Me.AcceptButton = Me.BTT_DOWN + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.CancelButton = Me.BTT_CANCEL + Me.ClientSize = New System.Drawing.Size(601, 271) + Me.Controls.Add(Me.TP_MAIN) + Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle + Me.Icon = Global.SCrawler.My.Resources.SiteYouTube.YouTubeIcon_32 + Me.KeyPreview = True + Me.MaximizeBox = False + Me.MinimizeBox = False + Me.Name = "VideoOptionsForm" + Me.ShowInTaskbar = False + Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide + Me.Text = "Download video" + TP_HEADER.ResumeLayout(False) + CType(Me.ICON_VIDEO, System.ComponentModel.ISupportInitialize).EndInit() + TP_HEADER_INFO.ResumeLayout(False) + Me.TP_HEADER_INFO_2.ResumeLayout(False) + Me.TP_HEADER_INFO_2.PerformLayout() + CType(ICON_CLOCK, System.ComponentModel.ISupportInitialize).EndInit() + CType(ICON_LINK, System.ComponentModel.ISupportInitialize).EndInit() + TP_FOOTER.ResumeLayout(False) + TP_DESTINATION.ResumeLayout(False) + TP_DESTINATION.PerformLayout() + TP_OK_CANCEL.ResumeLayout(False) + TP_WHAT.ResumeLayout(False) + TP_WHAT.PerformLayout() + Me.TP_HEADER_BASE.ResumeLayout(False) + Me.TP_SUBS.ResumeLayout(False) + Me.TP_SUBS.PerformLayout() + CType(Me.TXT_SUBS, System.ComponentModel.ISupportInitialize).EndInit() + Me.TP_MAIN.ResumeLayout(False) + Me.TP_OPTIONS.ResumeLayout(False) + Me.TP_OPTIONS.PerformLayout() + CType(Me.NUM_RES, System.ComponentModel.ISupportInitialize).EndInit() + CType(Me.TXT_SUBS_ADDIT, System.ComponentModel.ISupportInitialize).EndInit() + CType(Me.TXT_EXTRA_AUDIO_FORMATS, System.ComponentModel.ISupportInitialize).EndInit() + Me.ResumeLayout(False) + + End Sub + Private WithEvents ICON_VIDEO As PictureBox + Private WithEvents LBL_TITLE As Label + Private WithEvents LBL_TIME As Label + Private WithEvents LBL_URL As LinkLabel + Private WithEvents TP_OPTIONS As TableLayoutPanel + Private WithEvents LBL_AUDIO_CODEC As Label + Private WithEvents TP_SUBS As TableLayoutPanel + Private WithEvents NUM_RES As NumericUpDown + Private WithEvents TP_HEADER_BASE As TableLayoutPanel + Private WithEvents TP_CONTROLS As TableLayoutPanel + Private WithEvents TP_MAIN As TableLayoutPanel + Private WithEvents CMB_AUDIO_CODEC As ComboBox + Private WithEvents OPT_VIDEO As RadioButton + Private WithEvents OPT_AUDIO As RadioButton + Private WithEvents CMB_FORMAT As ComboBox + Private WithEvents TXT_SUBS As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents CMB_SUBS_FORMAT As ComboBox + Private WithEvents TXT_SUBS_ADDIT As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents TXT_EXTRA_AUDIO_FORMATS As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents TXT_FILE As TextBox + Private WithEvents BTT_BROWSE As Button + Private WithEvents BTT_DOWN As Button + Private WithEvents BTT_CANCEL As Button + Private WithEvents TP_HEADER_INFO_2 As TableLayoutPanel + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOptionsForm.resx b/SCrawler.YouTube/Controls/VideoOptionsForm.resx new file mode 100644 index 0000000..f3ef8e9 --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOptionsForm.resx @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + 17, 17 + + + False + + + False + + + False + + + False + + + False + + + False + + + False + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElE + QVQ4T2P4//8/QczOJyyqHpzfiE0OQwAZC8iqszAzs7CJ69o4BR768V/W2jcGXQ0KB4aFNS3dDQtnrbCb + ePCK48wTN1wXXXzge/jXf/clV55zC4hIIatF0cjIyMikElzc57z0wX+XHd/+2+//99/ywP//xlu//tdb + +eK/4Zp3/1WTOhYzARViNUAluKjTdf37/0ZTTn9TbdhwXblhwwW1/qOP1Ja9+K8w+95/6cm3/6v2Xvkv + qKjniGGAoIqRpW3/4e8S9uGdzFz82gwMDFxAzCxm4ZegtuLDf+VJ1/8rZM25IqLvnM/CximCYYCic1QN + v7x2JIwPwyrJ3XNUylddE9G2TWNmZOBDl4czmJiZMSRBmFdSyYyJgUEQmxwIYxWEYXZBCUls4sgYq6CA + prWNbtG8nXKeaVPR5XiVjSxEzf0yYXy4BBMLO6eQjoOXZvrkbbazrv53Xf/2v4CSbjBMXkhBl1/CMyNZ + qWnvGy5pNQ+YONwAfjXzAOupl/47LLr333L50/96q9/8l23YdES6cO5KuYqVW+R7Tj6SnfP0v4hryjyY + HhQDmFjYeHVKFp7WX/Xuv9Kq9/+Vd/z7r7rv/3+l7f//y676DEwDN/9L+BVvYkKLCTgDhNkkVUyVlr74 + qbbz73/VOTc/qsy89kWx+9h7qbQpJwS1bbOAscGGrB6EUTggLOqf16C55ft/HlnNAFZOXgVWdi4FRgYG + VnR1MIwhwMTCyqEQ37qEmZVDFF0OE/9nAACtFF4Ey6OP+wAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElE + QVQ4T2P4//8/QczOJyyqHpzfiE0OQwAZC8iqszAzs7CJ69o4BR768V/W2jcGXQ0KB4aFNS3dDQtnrbCb + ePCK48wTN1wXXXzge/jXf/clV55zC4hIIatF0cjIyMikElzc57z0wX+XHd/+2+//99/ywP//xlu//tdb + +eK/4Zp3/1WTOhYzARViNUAluKjTdf37/0ZTTn9TbdhwXblhwwW1/qOP1Ja9+K8w+95/6cm3/6v2Xvkv + qKjniGGAoIqRpW3/4e8S9uGdzFz82gwMDFxAzCxm4ZegtuLDf+VJ1/8rZM25IqLvnM/CximCYYCic1QN + v7x2JIwPwyrJ3XNUylddE9G2TWNmZOBDl4czmJiZMSRBmFdSyYyJgUEQmxwIYxWEYXZBCUls4sgYq6CA + prWNbtG8nXKeaVPR5XiVjSxEzf0yYXy4BBMLO6eQjoOXZvrkbbazrv53Xf/2v4CSbjBMXkhBl1/CMyNZ + qWnvGy5pNQ+YONwAfjXzAOupl/47LLr333L50/96q9/8l23YdES6cO5KuYqVW+R7Tj6SnfP0v4hryjyY + HhQDmFjYeHVKFp7WX/Xuv9Kq9/+Vd/z7r7rv/3+l7f//y676DEwDN/9L+BVvYkKLCTgDhNkkVUyVlr74 + qbbz73/VOTc/qsy89kWx+9h7qbQpJwS1bbOAscGGrB6EUTggLOqf16C55ft/HlnNAFZOXgVWdi4FRgYG + VnR1MIwhwMTCyqEQ37qEmZVDFF0OE/9nAACtFF4Ey6OP+wAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAR5JREFUOE+VkjFqwzAUhn2D9iShRyi+QhYbGujg3ZATZPKYdC6FQhPwlAMkg3dP + WQwhyWIyJIUW5NqyPb7oCVtIlhVTwYf8nv7/t2zJagel9KmqKsIACYL9RjI8UHz5zshougZr/AEvbxEP + aZCDBY3VslixaJvX3wzkkDiOwbZtDRGA5vdNAg+TL27qgmt5XkBG/gTdAG7Gt+3PP9oOaEGFCVEC6rp+ + 5g9MfM/c5e4OsEZMZkQEtGL5H2DdZ5JRArDwPA+iKII0TfkC9vroC9j5vq8JTWw3WzWgLMtZGIaa0MR8 + vlAD8PYlSaIJTTiOowY0p0Bc19XEJo6HE59FAPuMzyAINKGJ1XLFZxHALtMrnkBXOIQIIIQ8YvF/KrgB + cMaRN0UdBBkAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElE + QVQ4T2P4//8/QczOJyyqHpzfiE0OQwAZC8iqszAzs7CJ69o4BR768V/W2jcGXQ0KB4aFNS3dDQtnrbCb + ePCK48wTN1wXXXzge/jXf/clV55zC4hIIatF0cjIyMikElzc57z0wX+XHd/+2+//99/ywP//xlu//tdb + +eK/4Zp3/1WTOhYzARViNUAluKjTdf37/0ZTTn9TbdhwXblhwwW1/qOP1Ja9+K8w+95/6cm3/6v2Xvkv + qKjniGGAoIqRpW3/4e8S9uGdzFz82gwMDFxAzCxm4ZegtuLDf+VJ1/8rZM25IqLvnM/CximCYYCic1QN + v7x2JIwPwyrJ3XNUylddE9G2TWNmZOBDl4czmJiZMSRBmFdSyYyJgUEQmxwIYxWEYXZBCUls4sgYq6CA + prWNbtG8nXKeaVPR5XiVjSxEzf0yYXy4BBMLO6eQjoOXZvrkbbazrv53Xf/2v4CSbjBMXkhBl1/CMyNZ + qWnvGy5pNQ+YONwAfjXzAOupl/47LLr333L50/96q9/8l23YdES6cO5KuYqVW+R7Tj6SnfP0v4hryjyY + HhQDmFjYeHVKFp7WX/Xuv9Kq9/+Vd/z7r7rv/3+l7f//y676DEwDN/9L+BVvYkKLCTgDhNkkVUyVlr74 + qbbz73/VOTc/qsy89kWx+9h7qbQpJwS1bbOAscGGrB6EUTggLOqf16C55ft/HlnNAFZOXgVWdi4FRgYG + VnR1MIwhwMTCyqEQ37qEmZVDFF0OE/9nAACtFF4Ey6OP+wAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/SCrawler.YouTube/Controls/VideoOptionsForm.vb b/SCrawler.YouTube/Controls/VideoOptionsForm.vb new file mode 100644 index 0000000..534f5ed --- /dev/null +++ b/SCrawler.YouTube/Controls/VideoOptionsForm.vb @@ -0,0 +1,474 @@ +' 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.ComponentModel +Imports PersonalUtilities.Forms +Imports PersonalUtilities.Forms.Controls +Imports PersonalUtilities.Forms.Controls.Base +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Tools +Imports SCrawler.API.YouTube.Base +Imports SCrawler.API.YouTube.Objects +Imports SCrawler.DownloadObjects.STDownloader +Imports UMTypes = SCrawler.Plugin.UserMediaTypes +Imports ADB = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons +Namespace API.YouTube.Controls + Friend Class VideoOptionsForm : Implements IDesignXMLContainer +#Region "Declarations" + Private MyView As FormView + Friend Property DesignXML As EContainer Implements IDesignXMLContainer.DesignXML + Private Property DesignXMLNodes As String() Implements IDesignXMLContainer.DesignXMLNodes + Private Property DesignXMLNodeName As String Implements IDesignXMLContainer.DesignXMLNodeName + Private Const ControlsRow As Integer = 6 + Private ReadOnly Property CNT_PROCESSOR As TableControlsProcessor + Friend Property MyContainer As YouTubeMediaContainerBase + Private Initialization As Boolean = True + Private ReadOnly IsSavedObject As Boolean +#End Region +#Region "Initializers" + Friend Sub New(ByVal Container As YouTubeMediaContainerBase, Optional ByVal IsSavedObject As Boolean = False) + InitializeComponent() + MyContainer = Container + CNT_PROCESSOR = New TableControlsProcessor(TP_CONTROLS) + Me.IsSavedObject = IsSavedObject + End Sub +#End Region +#Region "Form handlers" + Private Sub VideoOptionsForm_Load(sender As Object, e As EventArgs) Handles Me.Load + If Not DesignXML Is Nothing Then + MyView = New FormView(Me) With {.LocationOnly = True} + MyView.Import() + MyView.SetFormSize() + End If + + If Not MyContainer Is Nothing Then + With MyContainer + Dim i% + Dim arr$() = Nothing + Dim arrComparer As New FComparer(Of String)(Function(x, y) x.ToLower = y.ToLower) + Dim setDef As Action(Of ComboBox, String) = + Sub(ByVal cmb As ComboBox, ByVal compValue As String) + i = -1 + If Not compValue.IsEmptyString Then i = arr.ListIndexOf(compValue, arrComparer, EDP.ReturnValue) + If i >= 0 Then cmb.SelectedIndex = i Else cmb.SelectedIndex = 0 + End Sub + Dim __audioOnly As Boolean = False + Dim __optionValue$ + + If .HasElements Then + Text = "Playlist" + If Not .PlaylistTitle.IsEmptyString Or Not .Title.IsEmptyString Then Text &= $" - { .PlaylistTitle.IfNullOrEmpty(.Title)}" + TP_MAIN.Controls.Remove(TP_HEADER_BASE) + TP_MAIN.RowStyles(0).Height = 0 + Dim def% = If(IsSavedObject, .ArrayMaxResolution, MyYouTubeSettings.DefaultVideoDefinition.Value) + If IsSavedObject Then + __audioOnly = def = -2 + If def <= 0 Then def = MyYouTubeSettings.DefaultVideoDefinition + Else + If Not def.ValueBetween(-1, 10000) Then def = 1080 + End If + NUM_RES.Value = def + Else + TP_OPTIONS.Controls.Remove(NUM_RES) + TP_OPTIONS.ColumnStyles(3).Width = 0 + Dim img As Image = Nothing + Dim imgUrl$ = .ThumbnailUrlMedia + If Not imgUrl.IsEmptyString Then + img = ImageRenderer.GetImage(SFile.GetBytesFromNet(imgUrl, EDP.ReturnValue), EDP.ReturnValue) + If Not img Is Nothing Then ICON_VIDEO.Image = img : ICON_VIDEO.InitialImage = img + End If + LBL_TITLE.Text = .Title + LBL_TIME.Text = AConvert(Of String)(.Duration, TimeToStringProvider, String.Empty) + TP_HEADER_INFO_2.ColumnStyles(1).Width = MeasureTextDefault(LBL_TIME.Text, LBL_TIME.Font).Width + PaddingE.GetOf({LBL_TIME}).Horizontal + TP_HEADER_INFO_2.Refresh() + LBL_URL.Text = .URL + End If + + If .IsMusic Or __audioOnly Then + OPT_AUDIO.Checked = True + Else + OPT_VIDEO.Checked = True + End If + CMB_FORMAT.Enabled = OPT_VIDEO.Checked + + arr = AvailableVideoFormats + CMB_FORMAT.Items.AddRange(arr) + If IsSavedObject Then + __optionValue = .OutputVideoExtension.IfNullOrEmpty(MyYouTubeSettings.DefaultVideoFormat.Value) + Else + __optionValue = MyYouTubeSettings.DefaultVideoFormat.Value + End If + setDef(CMB_FORMAT, __optionValue) + + arr = AvailableAudioFormats + CMB_AUDIO_CODEC.Items.AddRange(arr) + If IsSavedObject Then + __optionValue = .OutputAudioCodec.IfNullOrEmpty(IIf(.IsMusic, MyYouTubeSettings.DefaultAudioCodecMusic.Value, MyYouTubeSettings.DefaultAudioCodec.Value)) + Else + __optionValue = IIf(.IsMusic, MyYouTubeSettings.DefaultAudioCodecMusic.Value, MyYouTubeSettings.DefaultAudioCodec.Value) + End If + setDef(CMB_AUDIO_CODEC, __optionValue) + + arr = AvailableSubtitlesFormats + CMB_SUBS_FORMAT.Items.AddRange(arr) + If IsSavedObject Then + __optionValue = .OutputSubtitlesFormat.IfNullOrEmpty(IIf(.IsMusic, "LRC", MyYouTubeSettings.DefaultSubtitlesFormat.Value)) + Else + __optionValue = IIf(.IsMusic, "LRC", MyYouTubeSettings.DefaultSubtitlesFormat.Value) + End If + setDef(CMB_SUBS_FORMAT, __optionValue) + + TP_SUBS.Enabled = .Subtitles.Count > 0 + TXT_SUBS_ADDIT.Enabled = .Subtitles.Count > 0 + RefillTextBoxes() + TXT_SUBS_ADDIT.Checked = .PostProcessing_OutputSubtitlesFormats.Count > 0 + TXT_EXTRA_AUDIO_FORMATS.Checked = .PostProcessing_OutputAudioFormats.Count > 0 + TXT_FILE.Text = .File + RefillList() + If OPT_VIDEO.Checked Then + ChangeFileExtension(CMB_FORMAT.Text) + Else + If .HasElements Then NUM_RES.Enabled = False + ChangeFileExtension(CMB_AUDIO_CODEC.Text) + End If + End With + End If + Initialization = False + End Sub + Private Sub VideoOptionsForm_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing + MyView.DisposeIfReady() + End Sub +#End Region +#Region "Refill" + Private Sub RefillList() + Dim i% + Dim h% = -1 + Dim rh% = IIf(MyContainer.HasElements, 60, 25) + Dim s As New Size(Width, h) + Dim CalculateSize As Action(Of Integer) = + Sub(ByVal InitHeight As Integer) + With TP_MAIN.RowStyles(ControlsRow) : .SizeType = SizeType.Absolute : .Height = InitHeight : End With + s.Height = InitHeight + For ii% = 0 To TP_MAIN.RowStyles.Count - 1 + If Not ii = ControlsRow And TP_MAIN.RowStyles(ii).SizeType = SizeType.Absolute Then s.Height += TP_MAIN.RowStyles(ii).Height + Next + s.Height += PaddingE.GetOf({TP_MAIN}).Vertical(TP_MAIN.RowStyles.Count - 3 - IIf(MyContainer.HasElements, 1, 0)) + End Sub + Dim __contentType As UMTypes = IIf(OPT_VIDEO.Checked, UMTypes.Video, UMTypes.Audio) + With TP_CONTROLS + If .Controls.Count > 0 Then + For Each cnt As Control In .Controls : cnt.Dispose() : Next + .Controls.Clear() + End If + .RowStyles.Clear() + .RowCount = 0 + End With + With MyContainer + Dim audio As MediaObject = Nothing + If __contentType = UMTypes.Video Then audio = .SelectedAudio + Dim data As IEnumerable(Of Control) + + If .HasElements Then + data = .Elements.Select(Function(ee) New MediaItem(ee) With {.Dock = DockStyle.Fill, .Checked = ee.Checked, .IgnoreDownloadState = True}) + Else + data = (From m As MediaObject In .Self.MediaObjects + Where m.Type = __contentType + Select New VideoOption(m, audio) With {.Dock = DockStyle.Fill, .Checked = m.Index = MyContainer.SelectedVideoIndex}) + End If + + If data.ListExists Then + With TP_CONTROLS + With .RowStyles + .Clear() + For i = 0 To data.Count - 1 : .Add(New RowStyle(SizeType.Absolute, rh)) : Next + .Add(New RowStyle(SizeType.AutoSize)) + End With + .RowCount = .RowStyles.Count + For i = 0 To data.Count - 1 : .Controls.Add(data(i), 0, i) : Next + .Controls.Cast(Of Control).ToList.ForEach(Sub(ByVal d As Control) + DirectCast(d, ISupportInitialize).EndInit() + If MyContainer.HasElements Then + With DirectCast(d, MediaItem) + AddHandler .CheckedChanged, AddressOf MediaItem_CheckedChanged + AddHandler .Click, AddressOf CNT_PROCESSOR.MediaItem_Click + AddHandler .KeyDown, AddressOf CNT_PROCESSOR.MediaItem_KeyDown + End With + End If + End Sub) + If MyContainer.HasElements Then + If .Controls.Count > 0 Then + Dim cIndx% = 0 + Dim c As Color + For Each cnt As MediaItem In .Controls + cIndx += 1 + If (cIndx Mod 2) = 0 Then c = SystemColors.ControlLight Else c = SystemColors.Window + cnt.BackColor = c + Next + End If + Else + If Not data.ListExists(Function(d As VideoOption) d.Checked) Then + If MyYouTubeSettings.DefaultVideoDefinition > 0 Then + For Each cnt As VideoOption In .Controls + If cnt.MyMedia.Height <= MyYouTubeSettings.DefaultVideoDefinition Then cnt.Checked = True : Exit For + Next + Else + DirectCast(.Controls(0), VideoOption).Checked = True + End If + End If + End If + End With + + h = data.Count * rh + PaddingE.GetOf({TP_CONTROLS}).Vertical(1.5) + CalculateSize(h) + Dim hh% = Screen.PrimaryScreen.WorkingArea.Height - 20 + If s.Height > hh Then + h = 5 * rh + PaddingE.GetOf({TP_CONTROLS}).Vertical(1.5) + CalculateSize(h) + With TP_CONTROLS + .AutoSizeMode = AutoSizeMode.GrowAndShrink + .AutoScroll = True + Dim p As Padding = .Padding + p.Right += 3 + .Padding = p + .VerticalScroll.Visible = True + .VerticalScroll.Enabled = True + .HorizontalScroll.Visible = False + .HorizontalScroll.Enabled = False + End With + End If + + With TP_CONTROLS + .PerformLayout() + .Select() + If .Controls.Count > 0 Then .Controls(0).Focus() + End With + End If + End With + + If s.Height = -1 Then s.Height = rh + MaximumSize = Nothing + MinimumSize = Nothing + Size = s + MinimumSize = Size + MaximumSize = Size + End Sub +#End Region +#Region "Media items' handlers" + Private Sub MediaItem_CheckedChanged(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + ControlInvokeFast(TP_CONTROLS, Sub() Container.Checked = Sender.Checked, EDP.None) + End Sub +#End Region +#Region "OK, Cancel" + Private Sub BTT_DOWN_Click(sender As Object, e As EventArgs) Handles BTT_DOWN.Click + Try + Dim f As SFile + If MyContainer.HasElements Then + f = TXT_FILE.Text.CSFileP + Else + f = TXT_FILE.Text + End If + If f.IsEmptyString Then Throw New ArgumentNullException("File", "The output file cannot be null") + With MyContainer + .OutputVideoExtension = CMB_FORMAT.Text.StringToLower + .OutputAudioCodec = CMB_AUDIO_CODEC.Text.StringToLower + .OutputSubtitlesFormat = CMB_SUBS_FORMAT.Text.StringToLower + + If Not .HasElements Then + Dim cntIndex% = -1 + With TP_CONTROLS.Controls + If .Count > 0 Then + For Each cnt As VideoOption In .Self + If cnt.Checked Then cntIndex = cnt.MyMedia.Index : Exit For + Next + End If + End With + If cntIndex = -1 Then Throw New ArgumentOutOfRangeException("Download option", "What to download is not selected") + If OPT_VIDEO.Checked Then + .SelectedVideoIndex = cntIndex + Else + .SelectedVideoIndex = -1 + .SelectedAudioIndex = cntIndex + End If + .File = f + .FileSetManually = True + .UpdateInfoFields() + '#If DEBUG Then + ' Debug.WriteLine(.Command(False)) + '#End If + Else + If OPT_AUDIO.Checked Then + .SetMaxResolution(-2) + Else + .SetMaxResolution(NUM_RES.Value) + End If + .File = f + End If + End With + + If MyYouTubeSettings.OutputPathAutoChange Then MyYouTubeSettings.OutputPath.Value = f + + DialogResult = DialogResult.OK + Close() + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog + EDP.ShowMainMsg, ex, $"Download {IIf(MyContainer.HasElements, "playlist", "video")}") + End Try + End Sub + Private Sub BTT_CANCEL_Click(sender As Object, e As EventArgs) Handles BTT_CANCEL.Click + DialogResult = DialogResult.Cancel + Close() + End Sub +#End Region +#Region "Controls' handlers" +#Region "Header" + Private Sub LBL_URL_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LBL_URL.LinkClicked + If Not LBL_URL.Text.IsEmptyString Then + Try : Process.Start(LBL_URL.Text) : Catch : End Try + End If + End Sub +#End Region +#Region "Settings" + Private Sub OPT_VIDEO_AUDIO_CheckedChanged(sender As Object, e As EventArgs) Handles OPT_VIDEO.CheckedChanged, OPT_AUDIO.CheckedChanged + If Not Initialization Then + CMB_FORMAT.Enabled = OPT_VIDEO.Checked + If MyContainer.HasElements Then + NUM_RES.Enabled = OPT_VIDEO.Checked + Else + RefillList() + If OPT_VIDEO.Checked Then + ChangeFileExtension(CMB_FORMAT.Text) + Else + ChangeFileExtension(CMB_AUDIO_CODEC.Text) + End If + End If + End If + End Sub + Private Sub CMB_FORMAT_SelectedIndexChanged(sender As Object, e As EventArgs) Handles CMB_FORMAT.SelectedIndexChanged + If Not Initialization AndAlso OPT_VIDEO.Checked Then ChangeFileExtension(CMB_FORMAT.Text) + End Sub + Private Sub CMB_AUDIO_CODEC_SelectedIndexChanged(sender As Object, e As EventArgs) Handles CMB_AUDIO_CODEC.SelectedIndexChanged + If Not Initialization AndAlso OPT_AUDIO.Checked Then ChangeFileExtension(CMB_AUDIO_CODEC.Text) + End Sub + Private Sub ChangeFileExtension(ByVal NewExt As String) + If Not MyContainer.HasElements Then + Dim f As SFile = TXT_FILE.Text + f.Extension = NewExt.StringToLower + TXT_FILE.Text = f + End If + End Sub + Private Sub TXT_SUBS_ActionOnButtonClick(ByVal Sender As ActionButton, ByVal e As ActionButtonEventArgs) Handles TXT_SUBS.ActionOnButtonClick + Select Case Sender.DefaultButton + Case ADB.Open + If MyContainer.Subtitles.Count > 0 Then + Using f As New SimpleListForm(Of String)(MyContainer.Subtitles.Select(Function(s) s.Name)) With { + .DesignXML = DesignXML, + .DesignXMLNodeName = SimpleArraysFormNode, + .Mode = SimpleListFormModes.CheckedItems, + .FormText = "Subtitles" + } + With MyContainer + If .SubtitlesSelectedIndexes.Count > 0 Then f.DataSelectedIndexes.AddRange(.SubtitlesSelectedIndexes) + If f.ShowDialog() = DialogResult.OK Then + .SubtitlesSelectedIndexes.Clear() + If f.DataResultIndexes.Count > 0 Then .SubtitlesSelectedIndexes.AddRange(f.DataResultIndexes) + RefillTextBoxes() + End If + End With + End Using + End If + Case ADB.Refresh + MyContainer.SubtitlesSelectedIndexesReset() + RefillTextBoxes() + Case ADB.Clear + MyContainer.SubtitlesSelectedIndexes.Clear() + RefillTextBoxes() + End Select + End Sub + Private Sub CONTROLS_ADDIT_ActionOnButtonClick(ByVal Sender As ActionButton, ByVal e As ActionButtonEventArgs) Handles TXT_SUBS_ADDIT.ActionOnButtonClick, + TXT_EXTRA_AUDIO_FORMATS.ActionOnButtonClick + Dim isSubs As Boolean = CStr(DirectCast(e.AssociatedControl, TextBoxExtended).Tag) = "s" + Select Case Sender.DefaultButton + Case ADB.Open + Using f As New SimpleListForm(Of String)(If(isSubs, AvailableSubtitlesFormats, AvailableAudioFormats)) With { + .DesignXML = DesignXML, + .DesignXMLNodeName = SimpleArraysFormNode, + .Mode = SimpleListFormModes.CheckedItems, + .FormText = DirectCast(e.AssociatedControl, TextBoxExtended).CaptionText + } + With MyContainer + With If(isSubs, .PostProcessing_OutputSubtitlesFormats, .PostProcessing_OutputAudioFormats) + If .Self.Count > 0 Then f.DataSelected.AddRange(.Self) + If f.ShowDialog() = DialogResult.OK Then + .Self.Clear() + If f.DataResultIndexes.Count > 0 Then .Self.AddRange(f.DataResult) + DirectCast(e.AssociatedControl, TextBoxExtended).Text = .ListToString + RefillTextBoxes() + End If + End With + End With + End Using + Case ADB.Refresh + If isSubs Then + MyContainer.PostProcessing_OutputSubtitlesFormats_Reset() + Else + MyContainer.PostProcessing_OutputAudioFormats_Reset() + End If + RefillTextBoxes() + Case ADB.Clear + If isSubs Then + MyContainer.PostProcessing_OutputSubtitlesFormats.Clear() + Else + MyContainer.PostProcessing_OutputAudioFormats.Clear() + End If + RefillTextBoxes() + End Select + End Sub +#End Region +#Region "Footer" + Private Sub BTT_BROWSE_Click(sender As Object, e As EventArgs) Handles BTT_BROWSE.Click + Dim f As SFile +#Disable Warning BC40000 + If MyContainer.HasElements Then + f = TXT_FILE.Text.CSFileP + f = SFile.SelectPath(f, "Select the destination of the video files", EDP.ReturnValue) + Else + f = TXT_FILE.Text + Dim sPattern$ = $"Video|{AvailableVideoFormats.Select(Function(vf) $"*.{vf.ToLower}").ListToString(";")}" & + $"|Audio|{AvailableAudioFormats.Select(Function(af) $"*.{af.ToLower}").ListToString(";")}" & + "|All Files|*.*" + f = SFile.SaveAs(f, "Select the destination of the video file",,, sPattern, EDP.ReturnValue) + End If +#Enable Warning + If Not f.IsEmptyString Then TXT_FILE.Text = f + End Sub +#End Region +#End Region +#Region "Functions" + Private Sub RefillTextBoxes() + With MyContainer + If .SubtitlesSelectedIndexes.Count > 0 Then + TXT_SUBS.Text = ListAddList(Nothing, .Subtitles.Select(Function(s, i) If(.SubtitlesSelectedIndexes.Contains(i), s.ID, String.Empty)), + LAP.NotContainsOnly, EDP.ReturnValue).ListToString(",") + Else + TXT_SUBS.Clear() + End If + If .PostProcessing_OutputSubtitlesFormats.Count > 0 Then + TXT_SUBS_ADDIT.Text = .PostProcessing_OutputSubtitlesFormats.ListToString + Else + TXT_SUBS_ADDIT.Clear() + End If + If .PostProcessing_OutputAudioFormats.Count > 0 Then + TXT_EXTRA_AUDIO_FORMATS.Text = .PostProcessing_OutputAudioFormats.ListToString + Else + TXT_EXTRA_AUDIO_FORMATS.Clear() + End If + End With + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Declarations.vb b/SCrawler.YouTube/Declarations.vb new file mode 100644 index 0000000..171e981 --- /dev/null +++ b/SCrawler.YouTube/Declarations.vb @@ -0,0 +1,112 @@ +' 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.Tools +Imports PersonalUtilities.Forms.Toolbars +Imports PersonalUtilities.Functions.RegularExpressions +Namespace API.YouTube + Public Module YTDeclarations + Public Const YouTubeSite As String = "YouTube" + Public Const YouTubeSiteKey As String = "AndyProgram_YouTube" + Public Const YouTubeSettingsFile As String = "Settings\SettingsYouTube.xml" + Public Const DownloaderDataFolderYouTube As String = DownloadObjects.STDownloader.DownloaderDataFolder & "YouTube\" + Friend Const YouTubeDownloadPathDefault As String = "YouTubeDownloads\" + Friend Const SimpleArraysFormNode As String = "SimpleFormatsChooserForm" + Public Property MyYouTubeSettings As Base.YouTubeSettings + Public Property MyCache As CacheKeeper + Friend ReadOnly Property MyCacheSettings As New CacheKeeper(DownloaderDataFolderYouTube) With {.DeleteCacheOnDispose = False, .DeleteRootOnDispose = False} + Public ReadOnly Property YouTubeCookieNetscapeFile As New SFile($"Settings\Responser_{YouTubeSite}_Cookies_Netscape.txt") + Friend ReadOnly Property AvailableSubtitlesFormats As String() + Get + Return {"ASS", "LRC", "SRT", "VTT"} + End Get + End Property + Friend ReadOnly Property AvailableVideoFormats As String() + Get + Return {"AVI", "FLV", "GIF", "MKV", "MOV", "MP4", "WEBM", "AAC", "AIFF", "ALAC", "FLAC", "M4A", "MKA", "MP3", "OGG", "OPUS", "VORBIS", "WAV"} + End Get + End Property + Friend ReadOnly Property AvailableAudioFormats As String() + Get + 'AC3 not supported + Return {"AC3", "AAC", "ALAC", "FLAC", "M4A", "MP3", "OPUS", "VORBIS", "WAV"} + End Get + End Property + Friend ReadOnly VideoSizeProvider As New ANumbers(ANumbers.Cultures.USA, ANumbers.Options.DecimalsTrim) With {.DeclaredError = EDP.ReturnValue} + Friend ReadOnly NumberProvider As New ANumbers(ANumbers.Cultures.Primitive) With {.DeclaredError = EDP.ReturnValue} + Friend ReadOnly DateBaseProvider As New ADateTime(ADateTime.Formats.BaseDateTime) + Friend ReadOnly DateAddedProvider As New ADateTime(ADateTime.Formats.yyyymmdd) With {.DateSeparator = String.Empty} + Friend ReadOnly TimeToStringProvider As IFormatProvider = New TimeToStringConverter + Friend ReadOnly TitleHtmlConverter As Func(Of String, String) = Function(Input) Input.StringRemoveWinForbiddenSymbols().StringTrim() + Friend ReadOnly ProgressProvider As IMyProgressNumberProvider = MyProgressNumberProvider.Percentage + Public ReadOnly TrueUrlRegEx As RParams = RParams.DM(Base.YouTubeFunctions.TrueUrlPattern, 0, EDP.ReturnValue) + Private Class TimeToStringConverter : Implements ICustomProvider + Private ReadOnly _Provider As New ADateTime("mm\:ss") With {.TimeParseMode = ADateTime.TimeModes.TimeSpan} + Private ReadOnly _ProviderWithHours As New ADateTime("h\:mm\:ss") With {.TimeParseMode = ADateTime.TimeModes.TimeSpan} + Private ReadOnly Property Provider(ByVal t As TimeSpan) As IFormatProvider + Get + Return If(t.Hours > 0, _ProviderWithHours, _Provider) + End Get + End Property + Private Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Implements ICustomProvider.Convert + If Not IsNothing(Value) Then + If TypeOf Value Is Nullable(Of TimeSpan) Then + With DirectCast(Value, Nullable(Of TimeSpan)) + If .HasValue Then Return AConvert(Of String)(.Value, Me.Provider(.Value), String.Empty) + End With + ElseIf TypeOf Value Is TimeSpan Then + Dim t As TimeSpan = Value + Return AConvert(Of String)(t, Me.Provider(t), String.Empty) + End If + End If + Return String.Empty + End Function + Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat + Throw New NotImplementedException("'GetFormat' is not available in the 'TimeToStringConverter' context") + End Function + End Class + Friend Class DurationXmlConverter : Implements ICustomProvider + Private Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Implements ICustomProvider.Convert + Try + If DestinationType Is GetType(String) Then + If IsNothing(Value) Then + Return 0 + ElseIf TypeOf Value Is TimeSpan Then + Return DirectCast(Value, TimeSpan).TotalSeconds + Else + Throw New Exception + End If + ElseIf DestinationType Is GetType(TimeSpan) Then + If IsNothing(Value) Then + Return New TimeSpan + ElseIf TypeOf Value Is String Then + If CStr(Value).IsEmptyString Then + Return New TimeSpan + Else + Return TimeSpan.FromSeconds(AConvert(Of Double)(Value, EDP.ThrowException)) + End If + ElseIf TypeOf Value Is Double Or IsNumeric(Value) Then + Return TimeSpan.FromSeconds(Value) + Else + Throw New Exception + End If + Else + Throw New Exception + End If + Catch ex As Exception + Throw New Exception($"Cannot convert {Value.GetType.Name} to {DestinationType.Name}", ex) + End Try + End Function + Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat + Throw New NotImplementedException("'GetFormat' is not available in the 'DurationXmlConverter' context") + End Function + End Class + End Module +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/IDownloaderSettings.vb b/SCrawler.YouTube/Downloader/IDownloaderSettings.vb new file mode 100644 index 0000000..14ae81f --- /dev/null +++ b/SCrawler.YouTube/Downloader/IDownloaderSettings.vb @@ -0,0 +1,20 @@ +' 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 DownloadObjects.STDownloader + Public Interface IDownloaderSettings + ReadOnly Property ShowNotifications As Boolean + ReadOnly Property ShowNotificationsEveryDownload As Boolean + ReadOnly Property MaxJobsCount As Integer + ReadOnly Property DownloadAutomatically As Boolean + ReadOnly Property RemoveDownloadedAutomatically As Boolean + ReadOnly Property OnItemDoubleClick As DoubleClickBehavior + ReadOnly Property OpenFolderInOtherProgram As Boolean + ReadOnly Property OpenFolderInOtherProgram_Command As String + End Interface +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/MediaItem.Designer.vb b/SCrawler.YouTube/Downloader/MediaItem.Designer.vb new file mode 100644 index 0000000..59365af --- /dev/null +++ b/SCrawler.YouTube/Downloader/MediaItem.Designer.vb @@ -0,0 +1,257 @@ +' 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 DownloadObjects.STDownloader + + Partial Public Class MediaItem : Inherits System.Windows.Forms.UserControl + + 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() + Me.components = New System.ComponentModel.Container() + Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MediaItem)) + Me.ICON_VIDEO = New System.Windows.Forms.PictureBox() + Me.CONTEXT_MAIN = New System.Windows.Forms.ContextMenuStrip(Me.components) + Me.BTT_DOWN = New System.Windows.Forms.ToolStripMenuItem() + Me.SEP_DOWN = New System.Windows.Forms.ToolStripSeparator() + Me.BTT_OPEN_FOLDER = New System.Windows.Forms.ToolStripMenuItem() + Me.SEP_FOLDER = New System.Windows.Forms.ToolStripSeparator() + Me.BTT_COPY_LINK = New System.Windows.Forms.ToolStripMenuItem() + Me.BTT_OPEN_IN_BROWSER = New System.Windows.Forms.ToolStripMenuItem() + Me.SEP_DOWN_AGAIN = New System.Windows.Forms.ToolStripSeparator() + Me.BTT_DOWN_AGAIN = New System.Windows.Forms.ToolStripMenuItem() + Me.SEP_DEL = New System.Windows.Forms.ToolStripSeparator() + Me.BTT_REMOVE_FROM_LIST = New System.Windows.Forms.ToolStripMenuItem() + Me.BTT_DELETE_FILE = New System.Windows.Forms.ToolStripMenuItem() + Me.TP_INFO = New System.Windows.Forms.TableLayoutPanel() + Me.TP_CHECKED_TITLE = New System.Windows.Forms.TableLayoutPanel() + Me.LBL_TITLE = New System.Windows.Forms.Label() + Me.CH_CHECKED = New System.Windows.Forms.CheckBox() + Me.BTT_VIEW_SETTINGS = New System.Windows.Forms.ToolStripMenuItem() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + TP_MAIN.SuspendLayout() + CType(Me.ICON_VIDEO, System.ComponentModel.ISupportInitialize).BeginInit() + Me.CONTEXT_MAIN.SuspendLayout() + Me.TP_INFO.SuspendLayout() + Me.TP_CHECKED_TITLE.SuspendLayout() + Me.SuspendLayout() + ' + 'TP_MAIN + ' + TP_MAIN.ColumnCount = 2 + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 125.0!)) + TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.Controls.Add(Me.ICON_VIDEO, 0, 0) + TP_MAIN.Controls.Add(Me.TP_INFO, 1, 0) + TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + TP_MAIN.Location = New System.Drawing.Point(0, 0) + TP_MAIN.Margin = New System.Windows.Forms.Padding(0) + TP_MAIN.Name = "TP_MAIN" + TP_MAIN.RowCount = 1 + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 65.0!)) + TP_MAIN.Size = New System.Drawing.Size(549, 65) + TP_MAIN.TabIndex = 0 + AddHandler TP_MAIN.Click, AddressOf Me.Controls_Click + ' + 'ICON_VIDEO + ' + Me.ICON_VIDEO.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom + Me.ICON_VIDEO.ContextMenuStrip = Me.CONTEXT_MAIN + Me.ICON_VIDEO.Dock = System.Windows.Forms.DockStyle.Fill + Me.ICON_VIDEO.Location = New System.Drawing.Point(3, 3) + Me.ICON_VIDEO.Name = "ICON_VIDEO" + Me.ICON_VIDEO.Size = New System.Drawing.Size(119, 59) + Me.ICON_VIDEO.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom + Me.ICON_VIDEO.TabIndex = 0 + Me.ICON_VIDEO.TabStop = False + ' + 'CONTEXT_MAIN + ' + Me.CONTEXT_MAIN.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.BTT_DOWN, Me.SEP_DOWN, Me.BTT_OPEN_FOLDER, Me.SEP_FOLDER, Me.BTT_COPY_LINK, Me.BTT_OPEN_IN_BROWSER, Me.SEP_DOWN_AGAIN, Me.BTT_DOWN_AGAIN, Me.BTT_VIEW_SETTINGS, Me.SEP_DEL, Me.BTT_REMOVE_FROM_LIST, Me.BTT_DELETE_FILE}) + Me.CONTEXT_MAIN.Name = "CONTEXT_MAIN" + Me.CONTEXT_MAIN.ShowItemToolTips = False + Me.CONTEXT_MAIN.Size = New System.Drawing.Size(185, 226) + ' + 'BTT_DOWN + ' + Me.BTT_DOWN.Image = Global.SCrawler.My.Resources.Resources.ArrowDownPic_Blue_24 + Me.BTT_DOWN.Name = "BTT_DOWN" + Me.BTT_DOWN.Size = New System.Drawing.Size(184, 22) + Me.BTT_DOWN.Text = "Download" + ' + 'SEP_DOWN + ' + Me.SEP_DOWN.Name = "SEP_DOWN" + Me.SEP_DOWN.Size = New System.Drawing.Size(181, 6) + ' + 'BTT_OPEN_FOLDER + ' + Me.BTT_OPEN_FOLDER.Image = CType(resources.GetObject("BTT_OPEN_FOLDER.Image"), System.Drawing.Image) + Me.BTT_OPEN_FOLDER.Name = "BTT_OPEN_FOLDER" + Me.BTT_OPEN_FOLDER.Size = New System.Drawing.Size(184, 22) + Me.BTT_OPEN_FOLDER.Text = "Open folder" + ' + 'SEP_FOLDER + ' + Me.SEP_FOLDER.Name = "SEP_FOLDER" + Me.SEP_FOLDER.Size = New System.Drawing.Size(181, 6) + ' + 'BTT_COPY_LINK + ' + Me.BTT_COPY_LINK.Image = Global.SCrawler.My.Resources.Resources.LinkPic_32 + Me.BTT_COPY_LINK.Name = "BTT_COPY_LINK" + Me.BTT_COPY_LINK.Size = New System.Drawing.Size(184, 22) + Me.BTT_COPY_LINK.Text = "Copy link address" + ' + 'BTT_OPEN_IN_BROWSER + ' + Me.BTT_OPEN_IN_BROWSER.Image = CType(resources.GetObject("BTT_OPEN_IN_BROWSER.Image"), System.Drawing.Image) + Me.BTT_OPEN_IN_BROWSER.Name = "BTT_OPEN_IN_BROWSER" + Me.BTT_OPEN_IN_BROWSER.Size = New System.Drawing.Size(184, 22) + Me.BTT_OPEN_IN_BROWSER.Text = "Open link in browser" + ' + 'SEP_DOWN_AGAIN + ' + Me.SEP_DOWN_AGAIN.Name = "SEP_DOWN_AGAIN" + Me.SEP_DOWN_AGAIN.Size = New System.Drawing.Size(181, 6) + ' + 'BTT_DOWN_AGAIN + ' + Me.BTT_DOWN_AGAIN.Image = CType(resources.GetObject("BTT_DOWN_AGAIN.Image"), System.Drawing.Image) + Me.BTT_DOWN_AGAIN.Name = "BTT_DOWN_AGAIN" + Me.BTT_DOWN_AGAIN.Size = New System.Drawing.Size(184, 22) + Me.BTT_DOWN_AGAIN.Text = "Download again" + ' + 'SEP_DEL + ' + Me.SEP_DEL.Name = "SEP_DEL" + Me.SEP_DEL.Size = New System.Drawing.Size(181, 6) + ' + 'BTT_REMOVE_FROM_LIST + ' + Me.BTT_REMOVE_FROM_LIST.Image = CType(resources.GetObject("BTT_REMOVE_FROM_LIST.Image"), System.Drawing.Image) + Me.BTT_REMOVE_FROM_LIST.Name = "BTT_REMOVE_FROM_LIST" + Me.BTT_REMOVE_FROM_LIST.Size = New System.Drawing.Size(184, 22) + Me.BTT_REMOVE_FROM_LIST.Text = "Remove from the list" + ' + 'BTT_DELETE_FILE + ' + Me.BTT_DELETE_FILE.Image = CType(resources.GetObject("BTT_DELETE_FILE.Image"), System.Drawing.Image) + Me.BTT_DELETE_FILE.Name = "BTT_DELETE_FILE" + Me.BTT_DELETE_FILE.Size = New System.Drawing.Size(184, 22) + Me.BTT_DELETE_FILE.Text = "Delete file" + ' + 'TP_INFO + ' + Me.TP_INFO.ColumnCount = 1 + Me.TP_INFO.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_INFO.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + Me.TP_INFO.Controls.Add(Me.TP_CHECKED_TITLE, 0, 0) + Me.TP_INFO.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_INFO.Location = New System.Drawing.Point(125, 0) + Me.TP_INFO.Margin = New System.Windows.Forms.Padding(0) + Me.TP_INFO.Name = "TP_INFO" + Me.TP_INFO.RowCount = 2 + Me.TP_INFO.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + Me.TP_INFO.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50.0!)) + Me.TP_INFO.Size = New System.Drawing.Size(424, 65) + Me.TP_INFO.TabIndex = 1 + ' + 'TP_CHECKED_TITLE + ' + Me.TP_CHECKED_TITLE.ColumnCount = 2 + Me.TP_CHECKED_TITLE.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + Me.TP_CHECKED_TITLE.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_CHECKED_TITLE.Controls.Add(Me.LBL_TITLE, 1, 0) + Me.TP_CHECKED_TITLE.Controls.Add(Me.CH_CHECKED, 0, 0) + Me.TP_CHECKED_TITLE.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_CHECKED_TITLE.Location = New System.Drawing.Point(0, 0) + Me.TP_CHECKED_TITLE.Margin = New System.Windows.Forms.Padding(0) + Me.TP_CHECKED_TITLE.Name = "TP_CHECKED_TITLE" + Me.TP_CHECKED_TITLE.RowCount = 1 + Me.TP_CHECKED_TITLE.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_CHECKED_TITLE.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 32.0!)) + Me.TP_CHECKED_TITLE.Size = New System.Drawing.Size(424, 32) + Me.TP_CHECKED_TITLE.TabIndex = 0 + ' + 'LBL_TITLE + ' + Me.LBL_TITLE.ContextMenuStrip = Me.CONTEXT_MAIN + Me.LBL_TITLE.Dock = System.Windows.Forms.DockStyle.Fill + Me.LBL_TITLE.Font = New System.Drawing.Font("Arial", 9.0!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, CType(204, Byte)) + Me.LBL_TITLE.Location = New System.Drawing.Point(23, 0) + Me.LBL_TITLE.Name = "LBL_TITLE" + Me.LBL_TITLE.Size = New System.Drawing.Size(398, 32) + Me.LBL_TITLE.TabIndex = 1 + Me.LBL_TITLE.Text = "Video title" + Me.LBL_TITLE.TextAlign = System.Drawing.ContentAlignment.MiddleLeft + ' + 'CH_CHECKED + ' + Me.CH_CHECKED.AutoSize = True + Me.CH_CHECKED.Dock = System.Windows.Forms.DockStyle.Fill + Me.CH_CHECKED.Location = New System.Drawing.Point(3, 3) + Me.CH_CHECKED.Name = "CH_CHECKED" + Me.CH_CHECKED.Size = New System.Drawing.Size(14, 26) + Me.CH_CHECKED.TabIndex = 0 + Me.CH_CHECKED.UseVisualStyleBackColor = True + ' + 'BTT_VIEW_SETTINGS + ' + Me.BTT_VIEW_SETTINGS.Image = Global.SCrawler.My.Resources.Resources.SettingsPic_16 + Me.BTT_VIEW_SETTINGS.Name = "BTT_VIEW_SETTINGS" + Me.BTT_VIEW_SETTINGS.Size = New System.Drawing.Size(184, 22) + Me.BTT_VIEW_SETTINGS.Text = "View settings" + ' + 'MediaItem + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.Controls.Add(TP_MAIN) + Me.Name = "MediaItem" + Me.Size = New System.Drawing.Size(549, 65) + TP_MAIN.ResumeLayout(False) + CType(Me.ICON_VIDEO, System.ComponentModel.ISupportInitialize).EndInit() + Me.CONTEXT_MAIN.ResumeLayout(False) + Me.TP_INFO.ResumeLayout(False) + Me.TP_CHECKED_TITLE.ResumeLayout(False) + Me.TP_CHECKED_TITLE.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Private WithEvents ICON_VIDEO As PictureBox + Private WithEvents LBL_TITLE As Label + Private WithEvents CONTEXT_MAIN As ContextMenuStrip + Private WithEvents BTT_OPEN_FOLDER As ToolStripMenuItem + Private WithEvents BTT_COPY_LINK As ToolStripMenuItem + Private WithEvents BTT_OPEN_IN_BROWSER As ToolStripMenuItem + Private WithEvents BTT_DOWN_AGAIN As ToolStripMenuItem + Private WithEvents BTT_REMOVE_FROM_LIST As ToolStripMenuItem + Private WithEvents BTT_DELETE_FILE As ToolStripMenuItem + Private WithEvents BTT_DOWN As ToolStripMenuItem + Private WithEvents SEP_DOWN As ToolStripSeparator + Private WithEvents TP_INFO As TableLayoutPanel + Friend WithEvents TP_CHECKED_TITLE As TableLayoutPanel + Private WithEvents CH_CHECKED As CheckBox + Private WithEvents SEP_FOLDER As ToolStripSeparator + Private WithEvents SEP_DOWN_AGAIN As ToolStripSeparator + Private WithEvents SEP_DEL As ToolStripSeparator + Private WithEvents BTT_VIEW_SETTINGS As ToolStripMenuItem + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/MediaItem.resx b/SCrawler.YouTube/Downloader/MediaItem.resx new file mode 100644 index 0000000..e269219 --- /dev/null +++ b/SCrawler.YouTube/Downloader/MediaItem.resx @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + 17, 17 + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + wwAADsMBx2+oZAAAAk9JREFUOE+Nk0tIVGEUgKcWZlZWEK1aZEmZUlmpEQUtrF1Q0WNfUUgtCqKZLMXQ + UPJRmuY4TYYKmbPJcVSCAkuFQislzCx7rSo3Oc5T7zzu17njdZosxMXH/bn/+b97zn/ONfBjAJoPQctR + sB2bH1rsw8OEvnVj0BbUbdNJ19HW2+fmbgZO4wIR2MRm3irIQcsO2RA0Qe3mf9EOajGRuEycuXEiaDky + vdlXTfjrc0Kfu1BfVkJjtqR68G80cfRDIrgSL4Km/fCinFC/GU9JEs7CTUy25hAafYIy8hhl2IEy2Iwy + 0IzacRaq1kNNClizdEHjPhiw4nFcRClKFLukWZvG1PWVuI0GPMVr8LYb8XZcRvX/gqEH0HZSskmPEbyu + w9N+CaV41bS9/Qx86oQPdhh1EOwuwesw4bGdY7LvPoHhNgIlq5nIS5gl0DLoyIF3LfgbDjBhFap348uL + Q5EDyrVEgvZT+Htv4ytYiit/WYzAfh6lIgl1bBB/xQYmi1YQqkomVJmMapbLswhN2ajfX+ErW8dU+Vpc + eUtiBRcIPDpBoKcU99V4uJf1p2Va+2q3QE8hSvcN3PkJUL9r5g6kXW8sUoKRqSE7vlupBCUTrJl6u3SB + PNWfb/GWbyQQ2Z/pgjaJ9XsI91sIjr1H+fgUVeYh/KVrFs8Itp6O1B2RR+fAdhzupEHDXnw3U3GVpuAq + +w/y3l2wnHCNxMrhGIGMcp2WpkyYWeqcC30Co5OYu0gvwZIRtc4b606cpoUYtF9y3BgvNkFSmhcSO25a + TGCkk99shOQwl9bXawAAAABJRU5ErkJggg== + + + + + Qk02AwAAAAAAADYAAAAoAAAAEAAAABAAAAABABgAAAAAAAAAAADDDgAAww4AAAAAAAAAAAAA/wD//wD/ + /wD//wD/9vb29vb29vb29vb29vb29vb29vb29vb2/wD//wD//wD//wD//wD//wD/9vb29vb27t654bNN + 15UA15UA15UA15UA4bNN7t659vb29vb2/wD//wD//wD/9vb29vb25b9s15UA15UA15UA15UA15UA15UA + 15UA15UA5b9s9vb29vb2/wD//wD/9vb247lc15UA15UA5MJ43KYt7+nh7+nh3KYt5MJ415UA15UA5b9s + 9vb2/wD/9vb27t6515UA15UA7N7D69m015UA8e/w8e/w15UA69m07N7D15UA15UA7t659vb29vb24bNN + 15UA5MJ48e/w15UA7uTS8e/w8e/w7uTS15UA8e/w5MJ415UA4bNN9vb29vb215UA15UA15UA15UA15UA + 15UA15UA15UA15UA15UA15UA15UA15UA15UA9vb29vb215UA15UA8e/w8e/w15UA8e/w8e/w8e/w8e/w + 15UA8e/w8e/w15UA15UA9vb29vb215UA15UA8e/w8e/w15UA8e/w8e/w8e/w8e/w15UA8e/w8e/w15UA + 15UA9vb29vb215UA15UA15UA15UA15UA15UA15UA15UA15UA15UA15UA15UA15UA15UA9vb29vb24bNN + 15UA5MJ48e/w15UA7uTS8e/w8e/w7uTS15UA8e/w5MJ415UA4bNN9vb29vb27t6515UA15UA7N7D69m0 + 15UA8e/w8e/w15UA69m07N7D15UA15UA7t659vb2/wD/9vb25b9s15UA15UA5MJ43KYt7+nh7+nh3KYt + 5MJ415UA15UA5b9s9vb2/wD//wD/9vb29vb247lc15UA15UA15UA15UA15UA15UA15UA15UA5b9s9vb2 + 9vb2/wD//wD//wD/9vb29vb27t654bNN15UA15UA15UA15UA4bNN7t659vb29vb2/wD//wD//wD//wD/ + /wD//wD/9vb29vb29vb29vb29vb29vb29vb29vb2/wD//wD//wD//wD/ + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAADmUlE + QVRIS62WWWxMURjHL220JW1HausmlFrDFKUhnUGH6bRFzJ2idImlC0Vp2mlji1A8iNhCPIjIRES8EU+W + h2oEtbSDTk3HNNM7S01VKsXjkb/vXBo3k1Ee7sMvmZzzzf//ne/+z50RAAxL1MUIG4G/YAv3HSVhF5Vw + IYNdz3LadVj9RgdTB+HQYYPHIJuE1ocSdlEJFzG+1bPRLQLinglIeCkg+XUkKvz56hnkOfQs/rmA8S9H + YEp7FDI64tAQtKhnsMapZ7zzNHsUFnbGY4VzIk70l6hnIH4wsDR7NBZ3apDrSqL5T8eFgUr1DLZ78lim + Q4N8VzK29MxEpZSBa4M16hnU+c3M9CEFpdJsVHsXos63DDcHrf9nQEXD5VymwW/5USLNwl5vJhp7dTgW + NML2pR7jbsUMS+KdMTa5Q8NQxinfBU4dRFcOyjy52OtbhwOBDTgZLKPPmTgY0ON4MBdNfSbYBupxY8Aq + G10dqMG5/nIc7ytGQ6CQRliAamkTN/g1Ai4e95Qy3iogpX0UtBRDnhRzdxq2SXOxz5eFQ70rScCEU335 + ssGxj0YS06HSm4GN3ekwdE2C1hGH1LZR0JDOJof5jwHvnIvzTa0jlooTYfktvt+fhcOBHDQFTWRgxJGP + ObAGsulZLMLWnjlY756K5c4JmNcRi6T2SGheCIihS2l5ozAo6NRhMolnUAcGV6IcwwqvFrX+JTjYuwKH + SfRAYDms/mzs9y1GFe2VSnOw1j0FejqpLN4WCX4ZufiIBwLMLxQGm12rsLQzgWKYgmLPLNTQw6ynpDSS + IBet8y+TqaVRVdFIeJrWuCcj+/0EzH43BomvIhBLI45uFiDcJ+6QwROFwa6+Amb9bGFNg6Xs9Ncd7Oy3 + Knb2eyU7/20nu9y/m136tIvEl6BC0qKoZwby3alo9JVhj7T5R7m/kJVIIityi8zyXmTiW+I10SqyIQNb + uIgNwYuuf25kFd75KPKkI49OmUWnrfYWyXv/wBb2cijhhVf6a9lGei65XclYRDd6mj0GWz2iLBJaH0rY + RSVc5Eywmhm7kuQXHX+bJlBStrh+zTi0PpSwi0q4yNFAOVvgiEcKJWUsxZn/NhT+znlofShhF5VwkRpv + MUtti4KGYjj6sYCIh5QSu4oG27stjItHU+cjeQzvkcFzFQ2KnSKLoc4FukDCXeI2GbSoaFD4ziyPxNxK + 0AUyNxOP1DOwcaG/8I+/LRB+At7psBnyDBG0AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29m + dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAVoSURBVEhLhZVrTJNXGMdfrtNSQIoadKRz2o0CorU3 + WkDIVBRaaGNbwAteh+AARRQlitEYTTRekiX7sH3YPmyZH9wtziybigLRCWTaCW5sCBWhlrb0Ci9zSxbo + 2f+UliGX7SS/tO85z/k9T57zXhhCCPO7Wh3VIhB83JKQ0Nu4bNlHm5YseZ1hmHC69n+Y5HLFcz7/ft/S + pY+vr1hhwL4oEBJcZ0x793If5uZ+1VNfT/qvXCHP6+p8tzMymqRxcW8hMGKqbDo9MlmWddu2AfbiRTJ6 + +TIZKC52fyAUVi2JiYkLJmGaBYIPnx4+TPrOnCH9p08TC4LNx46RWwrF/ZXR0W/PleRZZuY669atZvbS + JcJiL9vQQEZPnSKmwkLPjcTE97GPB8KZlvh4C5X31dWRgRMniAVBtvPnyWB9ve+2XP7jmtjYpOlJTOnp + G60lJRZaOZWPQs4ePUpGUZh3xw7SnJDQhT0KEM3c5fOv9paVkX4kMAPL8ePEig1D584RG9rVpFS2rY6J + EQaTmKTSjbbiYsvIhQuERTGjKIrFvtHaWjK8fz9plsudexYu/BLxKsBj9ALBGzel0vt9e/b4XiBoENhQ + zRDOxIWWOY4cIS0KRZs4Nja5QyLJtRoM1pGzZ/0tYVExi/ayNTVkBPJ76enuJA7nM4j3gVWAHjgTIYqL + E96SStvMu3YR64EDxF5dTYYOHSJOJPNA5Kiu9rUrlZ1mrdbCnjzpr5jFGotYtqpqQi6TuVM4nKvwlYHU + gDzU31OMSGl8fPJtsbjVsn27z15RQRzAVVlJ3BB4kcx78CAZQbUjVIxrFtd+OdrbmpHhEXG5VE4rTwHz + wMRdFDw4jEgFj5dyRyRqsxYVEcfu3cQFPPv2ES8qHEbCYRzgsFZLvO+8Q7xKJXGDVoXCK46Ovob95YBW + Ph/8+xwE/wSTyHi81OZVq9qsGs2Ye8sW4srPJy6JhDgTE4kzOpo4IyKIMyyMOLhcX9Py5R4lj0cPtAKs + BBwwKfc7p174J5BEhHY9FIk6bBDaIRuiQkDFfsLDSbdU+pdBKPwe8e+BNDBD7vdNn6BYd+6stK5da7bP + nz9TDujcoEAw1lJY+CyFz9dCHDubnDJjwltRccS5fr3TjurnlIMBYE5NJY8Nhq7SrCwREsz6xL9y4S4v + b3Bt2uSyR0XNkDvQe9ouKu8HvaGh5FfQIxL5OgyG30qUStqmGUkm/3jKy0+48vLcs1XuiI8nL/Ly/rYl + JfmovCcgN4JW+l8iGe8oKuoqzcyckSQob3CpVB47l+sXv9KWxYtJt0r1x9ns7HZjQYHNnJxMfoH0EXgA + 7oFm0CmTjRsNhs6Na9bQF+Tkq57xlJXVu9Rqz9Bs8kWLSG9BwcsqieQONlXnpaaWdul0z7rR+6C8CTSC + m8Aol4+36/XGT7VaevCRIIRx6/WWoQULZq2cyveLxY0IrAT0IHm1OTmZT3Q6U2da2qT8B/Ad+BZ05OSM + GXW6p4hdBiIZZ1FRt5vPn6vyuwiqCsj9Xyq6qXbDBkWnXm/6OS3NN1X+dUgIeZSdPXZPoxlEXC6IY9pL + S7faNBqXC9Iplf95YBb5ZF+RpGbdunQcbO/D1avJ9YC8LT19/Iv8/BeqpKRPEDORAGNeY3HxSYtG43Eq + FL5etfpljUzWhPlZ5VOTlGVliR+hHUbs+0mpHP9GpRqM5XAuY20zmGgRRohYKIx9rNd/3qfTOa7l5uLu + C63BvARw6fp0eRCMyBslJe8+2bx58EFhoVMlFNJvgQ4kgggQEgykvV0ApEAd+J3z8Z8KxmuA3pr0zikA + b4LJZ2FqYBigFdOPNf0NC679Fxi0OPr+XxiAJgwURph/AJfOQQebMR8TAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29m + dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAVoSURBVEhLhZVrTJNXGMdfrtNSQIoadKRz2o0CorU3 + WkDIVBRaaGNbwAteh+AARRQlitEYTTRekiX7sH3YPmyZH9wtziybigLRCWTaCW5sCBWhlrb0Ci9zSxbo + 2f+UliGX7SS/tO85z/k9T57zXhhCCPO7Wh3VIhB83JKQ0Nu4bNlHm5YseZ1hmHC69n+Y5HLFcz7/ft/S + pY+vr1hhwL4oEBJcZ0x793If5uZ+1VNfT/qvXCHP6+p8tzMymqRxcW8hMGKqbDo9MlmWddu2AfbiRTJ6 + +TIZKC52fyAUVi2JiYkLJmGaBYIPnx4+TPrOnCH9p08TC4LNx46RWwrF/ZXR0W/PleRZZuY669atZvbS + JcJiL9vQQEZPnSKmwkLPjcTE97GPB8KZlvh4C5X31dWRgRMniAVBtvPnyWB9ve+2XP7jmtjYpOlJTOnp + G60lJRZaOZWPQs4ePUpGUZh3xw7SnJDQhT0KEM3c5fOv9paVkX4kMAPL8ePEig1D584RG9rVpFS2rY6J + EQaTmKTSjbbiYsvIhQuERTGjKIrFvtHaWjK8fz9plsudexYu/BLxKsBj9ALBGzel0vt9e/b4XiBoENhQ + zRDOxIWWOY4cIS0KRZs4Nja5QyLJtRoM1pGzZ/0tYVExi/ayNTVkBPJ76enuJA7nM4j3gVWAHjgTIYqL + E96SStvMu3YR64EDxF5dTYYOHSJOJPNA5Kiu9rUrlZ1mrdbCnjzpr5jFGotYtqpqQi6TuVM4nKvwlYHU + gDzU31OMSGl8fPJtsbjVsn27z15RQRzAVVlJ3BB4kcx78CAZQbUjVIxrFtd+OdrbmpHhEXG5VE4rTwHz + wMRdFDw4jEgFj5dyRyRqsxYVEcfu3cQFPPv2ES8qHEbCYRzgsFZLvO+8Q7xKJXGDVoXCK46Ovob95YBW + Ph/8+xwE/wSTyHi81OZVq9qsGs2Ye8sW4srPJy6JhDgTE4kzOpo4IyKIMyyMOLhcX9Py5R4lj0cPtAKs + BBwwKfc7p174J5BEhHY9FIk6bBDaIRuiQkDFfsLDSbdU+pdBKPwe8e+BNDBD7vdNn6BYd+6stK5da7bP + nz9TDujcoEAw1lJY+CyFz9dCHDubnDJjwltRccS5fr3TjurnlIMBYE5NJY8Nhq7SrCwREsz6xL9y4S4v + b3Bt2uSyR0XNkDvQe9ouKu8HvaGh5FfQIxL5OgyG30qUStqmGUkm/3jKy0+48vLcs1XuiI8nL/Ly/rYl + JfmovCcgN4JW+l8iGe8oKuoqzcyckSQob3CpVB47l+sXv9KWxYtJt0r1x9ns7HZjQYHNnJxMfoH0EXgA + 7oFm0CmTjRsNhs6Na9bQF+Tkq57xlJXVu9Rqz9Bs8kWLSG9BwcsqieQONlXnpaaWdul0z7rR+6C8CTSC + m8Aol4+36/XGT7VaevCRIIRx6/WWoQULZq2cyveLxY0IrAT0IHm1OTmZT3Q6U2da2qT8B/Ad+BZ05OSM + GXW6p4hdBiIZZ1FRt5vPn6vyuwiqCsj9Xyq6qXbDBkWnXm/6OS3NN1X+dUgIeZSdPXZPoxlEXC6IY9pL + S7faNBqXC9Iplf95YBb5ZF+RpGbdunQcbO/D1avJ9YC8LT19/Iv8/BeqpKRPEDORAGNeY3HxSYtG43Eq + FL5etfpljUzWhPlZ5VOTlGVliR+hHUbs+0mpHP9GpRqM5XAuY20zmGgRRohYKIx9rNd/3qfTOa7l5uLu + C63BvARw6fp0eRCMyBslJe8+2bx58EFhoVMlFNJvgQ4kgggQEgykvV0ApEAd+J3z8Z8KxmuA3pr0zikA + b4LJZ2FqYBigFdOPNf0NC679Fxi0OPr+XxiAJgwURph/AJfOQQebMR8TAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/MediaItem.vb b/SCrawler.YouTube/Downloader/MediaItem.vb new file mode 100644 index 0000000..e79b510 --- /dev/null +++ b/SCrawler.YouTube/Downloader/MediaItem.vb @@ -0,0 +1,468 @@ +' 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.ComponentModel +Imports SCrawler.API.YouTube +Imports SCrawler.API.YouTube.Objects +Imports SCrawler.API.YouTube.Controls +Imports PersonalUtilities.Tools +Imports PersonalUtilities.Forms.Toolbars +Namespace DownloadObjects.STDownloader + Public Delegate Sub MediaItemEventHandler(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + + Public Class MediaItem : Implements ISupportInitialize +#Region "Events" + Public Event DownloadStarted As MediaItemEventHandler + Public Event FileDownloaded As MediaItemEventHandler + Public Event Removal As MediaItemEventHandler + Public Event DownloadAgain As MediaItemEventHandler + Public Event DownloadRequested As MediaItemEventHandler + Public Event CheckedChanged As MediaItemEventHandler +#End Region +#Region "Declarations" +#Region "Controls" + Private WithEvents TP_CONTROLS As TableLayoutPanel + Private WithEvents TP_PROGRESS As TableLayoutPanel + Private WithEvents ICON_SITE As PictureBox + Private WithEvents ICON_CLOCK As PictureBox + Private WithEvents ICON_WHAT As PictureBox + Private WithEvents LBL_TIME As Label '54 + Private WithEvents ICON_SIZE As PictureBox + Private WithEvents LBL_SIZE As Label '68 + Private WithEvents LBL_INFO As Label + Private WithEvents LBL_PROGRESS As Label + Private WithEvents PR_MAIN As ProgressBar +#End Region + Private ReadOnly BindedControls As List(Of MediaItem) + Public Property MyContainer As IYouTubeMediaContainer + Private ReadOnly Property MyProgress As MyProgress + Public Property UseCookies As Boolean + Public Property Pending As Boolean = False + Public Property Checked As Boolean + Get + Return ControlInvokeFast(CH_CHECKED, Function() CH_CHECKED.Checked, False, EDP.ReturnValue) + End Get + Set(ByVal _Checked As Boolean) + ControlInvokeFast(CH_CHECKED, Sub() CH_CHECKED.Checked = _Checked, EDP.None) + End Set + End Property + Public Property IgnoreDownloadState As Boolean = False + Private ReadOnly FileOption As SFO = SFO.File +#End Region +#Region "Initializers" + Public Sub New() + InitializeComponent() + BindedControls = New List(Of MediaItem) + + CreateLabel(LBL_PROGRESS) + PR_MAIN = New ProgressBar With {.Anchor = AnchorStyles.Left + AnchorStyles.Right, .Size = New Size(.Size.Width, 18), .ContextMenuStrip = CONTEXT_MAIN} + TP_CONTROLS = New TableLayoutPanel With {.Dock = DockStyle.Fill, .ContextMenuStrip = CONTEXT_MAIN} + TP_PROGRESS = New TableLayoutPanel With {.Dock = DockStyle.Fill, .ContextMenuStrip = CONTEXT_MAIN} + With TP_PROGRESS + With .ColumnStyles + .Add(New ColumnStyle(SizeType.Percent, 40)) + .Add(New ColumnStyle(SizeType.Percent, 60)) + End With + .ColumnCount = .ColumnStyles.Count + .RowStyles.Add(New RowStyle(SizeType.Percent, 100)) + .RowCount = .RowStyles.Count + .Controls.Add(PR_MAIN, 0, 0) + .Controls.Add(LBL_PROGRESS, 1, 0) + End With + With TP_CONTROLS + .RowStyles.Add(New RowStyle(SizeType.Percent, 100)) + .RowCount = .RowStyles.Count + End With + + CreateIcon(ICON_SITE) + CreateIcon(ICON_WHAT) + CreateIcon(ICON_CLOCK, My.Resources.ClockPic_16) + CreateLabel(LBL_TIME) + CreateIcon(ICON_SIZE, My.Resources.RulerPic_32) + CreateLabel(LBL_SIZE) + CreateLabel(LBL_INFO) + + MyProgress = New MyProgress(PR_MAIN, LBL_PROGRESS) + End Sub + Private Sub CreateLabel(ByRef LBL As Label) + LBL = New Label With { + .Text = String.Empty, + .Margin = New Padding(0), + .AutoSize = False, + .Dock = DockStyle.Fill, + .TextAlign = ContentAlignment.MiddleLeft, + .Font = New Font("Arial", 9, FontStyle.Bold, GraphicsUnit.Point, 204), + .ForeColor = ForeColorLabels, + .ContextMenuStrip = CONTEXT_MAIN + } + End Sub + Private Sub CreateIcon(ByRef Obj As PictureBox, Optional ByVal Image As Image = Nothing) + Obj = New PictureBox With { + .Margin = New Padding(3), + .BackgroundImageLayout = ImageLayout.Zoom, + .SizeMode = PictureBoxSizeMode.Zoom, + .Dock = DockStyle.Fill, + .Image = Image, + .ContextMenuStrip = CONTEXT_MAIN + } + End Sub + Public Sub New(ByVal Container As IYouTubeMediaContainer) + Me.New + Const d$ = " " & ChrW(183) & " " + MyContainer = Container + MyContainer.Progress = MyProgress + If MyContainer.HasElements Then FileOption = SFO.Path Else FileOption = SFO.File + If Not MyContainer.SiteKey = YouTubeSiteKey Then + BTT_DOWN_AGAIN.Visible = False + SEP_DOWN_AGAIN.Visible = False + End If + + ICON_SITE.Image = MyContainer.SiteIcon + LBL_TIME.Text = AConvert(Of String)(Container.Duration, TimeToStringProvider, String.Empty) + LBL_TITLE.Text = Container.ToString(True) + If Not Container.SiteKey = YouTubeSiteKey And Container.ContentType = Plugin.UserMediaTypes.Picture Then + LBL_INFO.Text = Container.File.Extension.StringToUpper + ElseIf Not Container.IsMusic Then + If Container.Height > 0 Then + LBL_INFO.Text = $"{Container.File.Extension.StringToUpper}{d}{Container.Height}p" + Else + LBL_INFO.Text = Container.File.Extension.StringToUpper + End If + Else + If Container.Bitrate > 0 Then + LBL_INFO.Text = $"{Container.File.Extension.StringToUpper}{d}{Container.Bitrate}k" + Else + LBL_INFO.Text = Container.File.Extension.StringToUpper + End If + End If + UpdateMediaIcon() + End Sub +#End Region +#Region "Control handlers" + Private Sub MediaItem_Load(sender As Object, e As EventArgs) Handles Me.Load + RefillControls() + End Sub + Private Sub MediaItem_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed + BindedControls.Clear() + MyProgress.Dispose() + ICON_SITE.Dispose() + ICON_CLOCK.Dispose() + ICON_WHAT.Dispose() + LBL_TIME.Dispose() + ICON_SIZE.Dispose() + LBL_SIZE.Dispose() + LBL_INFO.Dispose() + LBL_PROGRESS.Dispose() + PR_MAIN.Dispose() + TP_CONTROLS.Controls.Clear() + TP_CONTROLS.Dispose() + TP_PROGRESS.Controls.Clear() + TP_PROGRESS.Dispose() + End Sub +#End Region +#Region "RefillControls" + Private Sub UpdateMediaIcon() + ControlInvokeFast(Me, Sub() + With MyContainer + If Not .SiteKey = YouTubeSiteKey And .ContentType = Plugin.UserMediaTypes.Picture Then + ICON_WHAT.Image = My.Resources.ImagePic_32 + ElseIf Not .IsMusic Then + ICON_WHAT.Image = My.Resources.VideoCamera_32 + Else + ICON_WHAT.Image = My.Resources.AudioMusic_32 + End If + End With + End Sub, EDP.None) + End Sub + Private Sub RefillControls() + ControlInvokeFast(Me, AddressOf RefillControlsImpl, EDP.None) + End Sub + Private Sub RefillControlsImpl() + With MyContainer + If ICON_VIDEO.Image Is Nothing Then + If .ThumbnailFile.Exists Then + ICON_VIDEO.Image = ImageRenderer.GetImage(SFile.GetBytes(.ThumbnailFile, EDP.ReturnValue), EDP.ReturnValue) + ElseIf Not .ThumbnailUrlMedia.IsEmptyString Then + ICON_VIDEO.Image = ImageRenderer.GetImage(SFile.GetBytesFromNet(.ThumbnailUrlMedia, EDP.ReturnValue), EDP.ReturnValue) + End If + End If + Dim s%, t% + Dim sv% = .Size / 1024 + If sv >= 1000 Then + LBL_SIZE.Text = AConvert(Of String)(sv / 1024, VideoSizeProvider) + LBL_SIZE.Text &= " GB" + Else + LBL_SIZE.Text = AConvert(Of String)(sv, VideoSizeProvider) + LBL_SIZE.Text &= " MB" + End If + If .Size > 0 Then + s = MeasureTextDefault(LBL_SIZE.Text, LBL_SIZE.Font).Width + Else + s = 0 + End If + If .Duration.TotalSeconds > 0 Then + t = MeasureTextDefault(LBL_TIME.Text, LBL_TIME.Font).Width + Else + t = 0 + End If + + LBL_TITLE.Text = MyContainer.ToString(True) + + If Not .SiteKey = YouTubeSiteKey Then BTT_VIEW_SETTINGS.Visible = False + + With TP_CONTROLS + .Controls.Clear() + .ColumnStyles.Clear() + .ColumnCount = 0 + If IgnoreDownloadState Or MyContainer.MediaState = Plugin.UserMediaStates.Downloaded Then + If Not MyContainer.SiteKey = YouTubeSiteKey Then UpdateMediaIcon() + If IgnoreDownloadState Then + BTT_OPEN_FOLDER.Visible = False + SEP_FOLDER.Visible = False + BTT_DOWN_AGAIN.Visible = False + SEP_DOWN_AGAIN.Visible = False + BTT_REMOVE_FROM_LIST.Visible = False + BTT_DELETE_FILE.Visible = False + SEP_DEL.Visible = False + End If + BTT_DOWN.Visible = False + SEP_DOWN.Visible = False + BTT_VIEW_SETTINGS.Visible = False + With .ColumnStyles + .Add(New ColumnStyle(SizeType.Absolute, 30)) + .Add(New ColumnStyle(SizeType.Absolute, 30)) + .Add(New ColumnStyle(SizeType.Absolute, IIf(t = 0, 0, 30))) + .Add(New ColumnStyle(SizeType.Absolute, t)) + .Add(New ColumnStyle(SizeType.Absolute, IIf(s = 0, 0, 30))) + .Add(New ColumnStyle(SizeType.Absolute, s)) + .Add(New ColumnStyle(SizeType.Percent, 100)) + End With + .ColumnCount = .ColumnStyles.Count + With .Controls + .Add(ICON_SITE, 0, 0) + .Add(ICON_WHAT, 1, 0) + If t > 0 Then + .Add(ICON_CLOCK, 2, 0) + .Add(LBL_TIME, 3, 0) + End If + If s > 0 Then + .Add(ICON_SIZE, 4, 0) + .Add(LBL_SIZE, 5, 0) + End If + .Add(LBL_INFO, 6, 0) + End With + Else + With .ColumnStyles + .Add(New ColumnStyle(SizeType.Absolute, 100)) + .Add(New ColumnStyle(SizeType.Percent, 100)) + End With + .ColumnCount = .ColumnStyles.Count + With .Controls + .Add(PR_MAIN, 0, 0) + .Add(LBL_PROGRESS, 1, 0) + End With + End If + End With + TP_INFO.Controls.Add(TP_CONTROLS, 0, 1) + BTT_OPEN_FOLDER.Enabled = .File.Exists(FileOption, False) + BTT_DELETE_FILE.Enabled = BTT_OPEN_FOLDER.Enabled + End With + End Sub +#End Region +#Region "Context buttons' handlers" + Public Sub AddToQueue() + ControlInvokeFast(Me, Sub() + BTT_DOWN.Visible = False + SEP_DOWN.Visible = False + End Sub, EDP.None) + End Sub + Private Sub BTT_DOWN_Click(sender As Object, e As EventArgs) Handles BTT_DOWN.Click + RaiseEvent DownloadRequested(Me, MyContainer) + End Sub + Public Sub Download(ByVal Token As Threading.CancellationToken) + Try + If Not MyContainer Is Nothing Then + RaiseEvent DownloadStarted(Me, MyContainer) + AddToQueue() + MyContainer.Download(UseCookies, Token) + MyContainer.Save() + Pending = False + RefillControls() + RaiseEvent FileDownloaded(Me, MyContainer) + End If + Catch dex As ObjectDisposedException When MyContainer.IsDisposed + Catch oex As OperationCanceledException When Token.IsCancellationRequested + Throw oex + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"MediaItem.Download:{vbCr}{MyContainer.ToString}{vbCr}{MyContainer.URL})") + End Try + End Sub +#End Region +#Region "Colors" + Private ReadOnly ForeColorLabels As Color = SystemColors.ControlDark + Private ForeColorDefault As Color + Public Overrides Property ForeColor As Color + Get + Return MyBase.ForeColor + End Get + Set(ByVal c As Color) + ForeColorDefault = c + MyBase.ForeColor = c + End Set + End Property + Private BackColorDefault As Color + Public Overrides Property BackColor As Color + Get + Return MyBase.BackColor + End Get + Set(ByVal c As Color) + BackColorDefault = c + MyBase.BackColor = c + End Set + End Property + Private IsActiveControl As Boolean = False + Private Sub DropColor() + IsActiveControl = False + MyBase.BackColor = BackColorDefault + MyBase.ForeColor = ForeColorDefault + ChangeLabelsColor(ForeColorLabels) + End Sub + Private Sub ChangeLabelsColor(ByVal ForeColor As Color) + ControlInvokeFast(Me, Sub() + LBL_TIME.ForeColor = ForeColor + LBL_SIZE.ForeColor = ForeColor + LBL_INFO.ForeColor = ForeColor + LBL_PROGRESS.ForeColor = ForeColor + End Sub, EDP.None) + End Sub +#End Region +#Region "Click handlers" + Public Sub PerformClick() + Controls_Click(Me, EventArgs.Empty) + End Sub + Private Sub Controls_Click(sender As Object, e As EventArgs) Handles ICON_VIDEO.MouseClick, CH_CHECKED.MouseClick, LBL_TITLE.MouseClick, TP_INFO.MouseClick, + TP_CONTROLS.MouseClick, TP_PROGRESS.MouseClick, ICON_SITE.MouseClick, ICON_CLOCK.MouseClick, + ICON_WHAT.MouseClick, LBL_TIME.MouseClick, ICON_SIZE.MouseClick, LBL_INFO.MouseClick, + LBL_PROGRESS.MouseClick, PR_MAIN.MouseClick, CONTEXT_MAIN.Opened + IsActiveControl = True + MyBase.BackColor = SystemColors.Highlight + MyBase.ForeColor = SystemColors.HighlightText + ChangeLabelsColor(SystemColors.HighlightText) + BindedControls.ForEach(Sub(c) c.DropColor()) + OnClick(e) + End Sub + Private Sub Controls_DoubleClick(sender As Object, e As EventArgs) Handles ICON_VIDEO.DoubleClick, LBL_TITLE.DoubleClick, TP_INFO.DoubleClick, + TP_CONTROLS.DoubleClick, TP_PROGRESS.DoubleClick, ICON_SITE.DoubleClick, ICON_CLOCK.DoubleClick, + ICON_WHAT.DoubleClick, LBL_TIME.DoubleClick, ICON_SIZE.DoubleClick, LBL_INFO.DoubleClick, + LBL_PROGRESS.DoubleClick, PR_MAIN.DoubleClick + Controls_Click(sender, e) + If Not IgnoreDownloadState AndAlso Not MyDownloaderSettings.OnItemDoubleClick = DoubleClickBehavior.None Then + Dim m As New MMessage("The specified path was not found.", "Open file/folder",, vbExclamation) + If MyDownloaderSettings.OnItemDoubleClick = DoubleClickBehavior.File Then + If FileOption = SFO.File And MyContainer.File.Exists(SFO.File, False) Then + MyContainer.File.Open(SFO.File,, EDP.ShowMainMsg) + ElseIf MyContainer.File.Exists(SFO.Path, False) Then + MyContainer.File.Open(SFO.Path,, EDP.ShowMainMsg) + Else + m.Show() + End If + Else + If MyContainer.File.Exists(SFO.Path, False) Then MyContainer.File.Open(SFO.Path,, EDP.ShowMainMsg) Else m.Show() + End If + End If + OnDoubleClick(e) + End Sub + Private Sub CH_CHECKED_CheckedChanged(sender As Object, e As EventArgs) Handles CH_CHECKED.CheckedChanged + RaiseEvent CheckedChanged(Me, MyContainer) + End Sub + Protected Overrides Function ProcessDialogKey(ByVal KeyData As Keys) As Boolean + If IsActiveControl Then + If KeyData = Keys.Down Or KeyData = Keys.Up Then + OnKeyDown(New KeyEventArgs(KeyData)) + Return True + Else + Return MyBase.ProcessDialogKey(KeyData) + End If + Else + Return False + End If + End Function +#End Region +#Region "Context buttons' handlers" + Private Sub BTT_OPEN_FOLDER_Click(sender As Object, e As EventArgs) Handles BTT_OPEN_FOLDER.Click + If MyContainer.File.Exists(FileOption, False) Then GlobalOpenPath(MyContainer.File) + End Sub + Private Sub BTT_COPY_LINK_Click(sender As Object, e As EventArgs) Handles BTT_COPY_LINK.Click + If Not MyContainer.URL.IsEmptyString Then + BufferText = MyContainer.URL + Else + MsgBoxE({"Media URL is not found", "Copy media URL"}, vbExclamation) + End If + End Sub + Private Sub BTT_OPEN_IN_BROWSER_Click(sender As Object, e As EventArgs) Handles BTT_OPEN_IN_BROWSER.Click + If Not MyContainer.URL_BASE.IsEmptyString Then + Try : Process.Start(MyContainer.URL_BASE) : Catch : End Try + Else + MsgBoxE({"Media URL is not found", "Open link in browser"}, vbExclamation) + End If + End Sub + Private Sub BTT_DOWN_AGAIN_Click(sender As Object, e As EventArgs) Handles BTT_DOWN_AGAIN.Click + RaiseEvent DownloadAgain(Me, MyContainer) + End Sub + Private Sub BTT_VIEW_SETTINGS_Click(sender As Object, e As EventArgs) Handles BTT_VIEW_SETTINGS.Click + If Not MyContainer Is Nothing Then + Dim f As Form = Nothing + Select Case MyContainer.ObjectType + Case Base.YouTubeMediaType.Single : f = New VideoOptionsForm(MyContainer, True) + Case Base.YouTubeMediaType.Channel, Base.YouTubeMediaType.PlayList + If MyContainer.IsMusic Then + f = New MusicPlaylistsForm(MyContainer) + Else + f = New VideoOptionsForm(MyContainer, True) + End If + End Select + If Not f Is Nothing Then + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then MyContainer.Save() + f.Dispose() + End If + End If + End Sub + Private Sub BTT_REMOVE_FROM_LIST_Click(sender As Object, e As EventArgs) Handles BTT_REMOVE_FROM_LIST.Click + RaiseEvent Removal(Me, MyContainer) + End Sub + Private Sub BTT_DELETE_FILE_Click(sender As Object, e As EventArgs) Handles BTT_DELETE_FILE.Click + If MsgBoxE({$"Are you sure you want to delete the following {FileOption.ToString.ToLower}:{vbCr}" & + If(FileOption = SFO.File, MyContainer.File.ToString, MyContainer.File.PathWithSeparator), + $"Deleting a {FileOption.ToString.ToLower}"}, vbExclamation,,, {"Process", "Cancel"}) = 0 Then + MyContainer.Delete(True) + RaiseEvent Removal(Me, MyContainer) + End If + End Sub +#End Region +#Region "ISupportInitialize Support" + Public Sub BeginInit() Implements ISupportInitialize.BeginInit + End Sub + Public Sub EndInit() Implements ISupportInitialize.EndInit + If Not Parent Is Nothing AndAlso TypeOf Parent Is TableLayoutPanel Then + With DirectCast(Parent, TableLayoutPanel) + If .Controls.Count > 0 Then + For Each cnt As Control In .Controls + If Not cnt Is Nothing AndAlso TypeOf cnt Is MediaItem AndAlso Not cnt Is Me Then + With DirectCast(cnt, MediaItem) + If Not BindedControls.Contains(cnt) Then BindedControls.Add(cnt) + End With + End If + Next + End If + End With + End If + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/Notificator.vb b/SCrawler.YouTube/Downloader/Notificator.vb new file mode 100644 index 0000000..4550236 --- /dev/null +++ b/SCrawler.YouTube/Downloader/Notificator.vb @@ -0,0 +1,32 @@ +' 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.Tools.Notifications +Namespace DownloadObjects.STDownloader + Public Interface INotificator + Sub Clear() + Sub ShowNotification(ByVal Text As String, ByVal Image As SFile) + End Interface + Friend Class YTNotificator : Implements INotificator + Private WithEvents Notificator As NotificationsManager + Private ReadOnly Property SourceForm As Form + Friend Sub New(ByRef Source As Form) + Notificator = New NotificationsManager + SourceForm = Source + End Sub + Friend Sub Clear() Implements INotificator.Clear + Notificator.Clear() + End Sub + Friend Sub ShowNotification(ByVal Text As String, ByVal Image As SFile) Implements INotificator.ShowNotification + If MyDownloaderSettings.ShowNotifications Then Notification.ShowNotification(Text,,, Image) + End Sub + Private Sub Notificator_OnClicked(ByVal Key As String) Handles Notificator.OnClicked + SourceForm.FormShowS + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/STDownloaderDeclarations.vb b/SCrawler.YouTube/Downloader/STDownloaderDeclarations.vb new file mode 100644 index 0000000..f21cd65 --- /dev/null +++ b/SCrawler.YouTube/Downloader/STDownloaderDeclarations.vb @@ -0,0 +1,20 @@ +' 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 DownloadObjects.STDownloader + Public Module STDownloaderDeclarations + Public Const DownloaderDataFolder As String = "Settings\DownloaderData\" + Public Enum DoubleClickBehavior As Integer + None = SFO.None + Folder = SFO.Path + File = SFO.File + End Enum + Public Property MyNotificator As INotificator + Public Property MyDownloaderSettings As IDownloaderSettings + End Module +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/VideoListForm.Designer.vb b/SCrawler.YouTube/Downloader/VideoListForm.Designer.vb new file mode 100644 index 0000000..bd7c91f --- /dev/null +++ b/SCrawler.YouTube/Downloader/VideoListForm.Designer.vb @@ -0,0 +1,306 @@ +' 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 DownloadObjects.STDownloader + + Partial Public Class VideoListForm : 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 SEP_2 As System.Windows.Forms.ToolStripSeparator + Dim SEP_3 As System.Windows.Forms.ToolStripSeparator + Dim MENU_ADD_SEP_1 As System.Windows.Forms.ToolStripSeparator + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(VideoListForm)) + Me.TOOLBAR_BOTTOM = New System.Windows.Forms.StatusStrip() + Me.PR_MAIN = New System.Windows.Forms.ToolStripProgressBar() + Me.LBL_INFO = New System.Windows.Forms.ToolStripStatusLabel() + Me.TP_CONTROLS = New System.Windows.Forms.TableLayoutPanel() + Me.TOOLBAR_TOP = New System.Windows.Forms.ToolStrip() + Me.BTT_SETTINGS = New System.Windows.Forms.ToolStripButton() + Me.SEP_1 = New System.Windows.Forms.ToolStripSeparator() + Me.MENU_ADD = New System.Windows.Forms.ToolStripDropDownButton() + Me.BTT_ADD = New PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick() + Me.BTT_ADD_PLS_ARR = New PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick() + Me.BTT_ADD_NO_SHORTS = New PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick() + Me.BTT_ADD_SHORTS_ONLY = New PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick() + Me.BTT_DOWN = New System.Windows.Forms.ToolStripButton() + Me.BTT_STOP = New System.Windows.Forms.ToolStripButton() + Me.BTT_DELETE = New System.Windows.Forms.ToolStripButton() + Me.BTT_CLEAR_DONE = New System.Windows.Forms.ToolStripButton() + Me.BTT_CLEAR_ALL = New System.Windows.Forms.ToolStripButton() + Me.SEP_LOG = New System.Windows.Forms.ToolStripSeparator() + Me.BTT_LOG = New System.Windows.Forms.ToolStripButton() + Me.BTT_INFO = New System.Windows.Forms.ToolStripButton() + Me.BTT_DONATE = New System.Windows.Forms.ToolStripButton() + SEP_2 = New System.Windows.Forms.ToolStripSeparator() + SEP_3 = New System.Windows.Forms.ToolStripSeparator() + MENU_ADD_SEP_1 = New System.Windows.Forms.ToolStripSeparator() + Me.TOOLBAR_BOTTOM.SuspendLayout() + Me.TOOLBAR_TOP.SuspendLayout() + Me.SuspendLayout() + ' + 'SEP_2 + ' + SEP_2.Name = "SEP_2" + SEP_2.Size = New System.Drawing.Size(6, 25) + ' + 'SEP_3 + ' + SEP_3.Name = "SEP_3" + SEP_3.Size = New System.Drawing.Size(6, 25) + ' + 'MENU_ADD_SEP_1 + ' + MENU_ADD_SEP_1.Name = "MENU_ADD_SEP_1" + MENU_ADD_SEP_1.Size = New System.Drawing.Size(181, 6) + ' + 'TOOLBAR_BOTTOM + ' + Me.TOOLBAR_BOTTOM.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.PR_MAIN, Me.LBL_INFO}) + Me.TOOLBAR_BOTTOM.Location = New System.Drawing.Point(0, 439) + Me.TOOLBAR_BOTTOM.Name = "TOOLBAR_BOTTOM" + Me.TOOLBAR_BOTTOM.Size = New System.Drawing.Size(584, 22) + Me.TOOLBAR_BOTTOM.TabIndex = 0 + ' + 'PR_MAIN + ' + Me.PR_MAIN.Name = "PR_MAIN" + Me.PR_MAIN.Size = New System.Drawing.Size(200, 16) + ' + 'LBL_INFO + ' + Me.LBL_INFO.Name = "LBL_INFO" + Me.LBL_INFO.Size = New System.Drawing.Size(0, 17) + ' + 'TP_CONTROLS + ' + Me.TP_CONTROLS.AutoScroll = True + Me.TP_CONTROLS.BackColor = System.Drawing.SystemColors.Window + Me.TP_CONTROLS.ColumnCount = 1 + Me.TP_CONTROLS.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_CONTROLS.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_CONTROLS.Location = New System.Drawing.Point(0, 25) + Me.TP_CONTROLS.Name = "TP_CONTROLS" + Me.TP_CONTROLS.RowCount = 1 + Me.TP_CONTROLS.RowStyles.Add(New System.Windows.Forms.RowStyle()) + Me.TP_CONTROLS.Size = New System.Drawing.Size(584, 414) + Me.TP_CONTROLS.TabIndex = 0 + ' + 'TOOLBAR_TOP + ' + Me.TOOLBAR_TOP.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden + Me.TOOLBAR_TOP.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.BTT_SETTINGS, Me.SEP_1, Me.MENU_ADD, SEP_2, Me.BTT_DOWN, Me.BTT_STOP, SEP_3, Me.BTT_DELETE, Me.BTT_CLEAR_DONE, Me.BTT_CLEAR_ALL, Me.SEP_LOG, Me.BTT_LOG, Me.BTT_INFO, Me.BTT_DONATE}) + Me.TOOLBAR_TOP.Location = New System.Drawing.Point(0, 0) + Me.TOOLBAR_TOP.Name = "TOOLBAR_TOP" + Me.TOOLBAR_TOP.Size = New System.Drawing.Size(584, 25) + Me.TOOLBAR_TOP.TabIndex = 2 + ' + 'BTT_SETTINGS + ' + Me.BTT_SETTINGS.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image + Me.BTT_SETTINGS.Image = Global.SCrawler.My.Resources.Resources.SettingsPic_16 + Me.BTT_SETTINGS.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_SETTINGS.Name = "BTT_SETTINGS" + Me.BTT_SETTINGS.Size = New System.Drawing.Size(23, 22) + Me.BTT_SETTINGS.Text = "Settings" + ' + 'SEP_1 + ' + Me.SEP_1.Name = "SEP_1" + Me.SEP_1.Size = New System.Drawing.Size(6, 25) + ' + 'MENU_ADD + ' + Me.MENU_ADD.AutoToolTip = False + Me.MENU_ADD.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.BTT_ADD, Me.BTT_ADD_PLS_ARR, MENU_ADD_SEP_1, Me.BTT_ADD_NO_SHORTS, Me.BTT_ADD_SHORTS_ONLY}) + Me.MENU_ADD.Image = CType(resources.GetObject("MENU_ADD.Image"), System.Drawing.Image) + Me.MENU_ADD.ImageTransparentColor = System.Drawing.Color.Magenta + Me.MENU_ADD.Name = "MENU_ADD" + Me.MENU_ADD.Size = New System.Drawing.Size(84, 22) + Me.MENU_ADD.Text = "Add (Ins)" + ' + 'BTT_ADD + ' + Me.BTT_ADD.AutoToolTip = True + Me.BTT_ADD.Image = CType(resources.GetObject("BTT_ADD.Image"), System.Drawing.Image) + Me.BTT_ADD.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_ADD.Name = "BTT_ADD" + Me.BTT_ADD.Size = New System.Drawing.Size(184, 22) + Me.BTT_ADD.Tag = "a" + Me.BTT_ADD.Text = "Add (Ins)" + Me.BTT_ADD.ToolTipText = "Click to add." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Ctrl+click to use cookies for download (if supported)." + ' + 'BTT_ADD_PLS_ARR + ' + Me.BTT_ADD_PLS_ARR.AutoToolTip = True + Me.BTT_ADD_PLS_ARR.Image = CType(resources.GetObject("BTT_ADD_PLS_ARR.Image"), System.Drawing.Image) + Me.BTT_ADD_PLS_ARR.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_ADD_PLS_ARR.Name = "BTT_ADD_PLS_ARR" + Me.BTT_ADD_PLS_ARR.Size = New System.Drawing.Size(184, 22) + Me.BTT_ADD_PLS_ARR.Tag = "pls" + Me.BTT_ADD_PLS_ARR.Text = "Add playlist array" + Me.BTT_ADD_PLS_ARR.ToolTipText = "Click to add." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Ctrl+click to use cookies for download (if supported)." + ' + 'BTT_ADD_NO_SHORTS + ' + Me.BTT_ADD_NO_SHORTS.AutoToolTip = True + Me.BTT_ADD_NO_SHORTS.Image = CType(resources.GetObject("BTT_ADD_NO_SHORTS.Image"), System.Drawing.Image) + Me.BTT_ADD_NO_SHORTS.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_ADD_NO_SHORTS.Name = "BTT_ADD_NO_SHORTS" + Me.BTT_ADD_NO_SHORTS.Size = New System.Drawing.Size(184, 22) + Me.BTT_ADD_NO_SHORTS.Tag = "ans" + Me.BTT_ADD_NO_SHORTS.Text = "Add (without Shorts)" + Me.BTT_ADD_NO_SHORTS.ToolTipText = "Download all videos except 'Shorts'." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Click to add." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Ctrl+click to use cookies fo" & + "r download (if supported)." + ' + 'BTT_ADD_SHORTS_ONLY + ' + Me.BTT_ADD_SHORTS_ONLY.AutoToolTip = True + Me.BTT_ADD_SHORTS_ONLY.Image = CType(resources.GetObject("BTT_ADD_SHORTS_ONLY.Image"), System.Drawing.Image) + Me.BTT_ADD_SHORTS_ONLY.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_ADD_SHORTS_ONLY.Name = "BTT_ADD_SHORTS_ONLY" + Me.BTT_ADD_SHORTS_ONLY.Size = New System.Drawing.Size(184, 22) + Me.BTT_ADD_SHORTS_ONLY.Tag = "as" + Me.BTT_ADD_SHORTS_ONLY.Text = "Add (Shorts only)" + Me.BTT_ADD_SHORTS_ONLY.ToolTipText = "Download only 'Shorts' videos." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Click to add." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Ctrl+click to use cookies for down" & + "load (if supported)." + ' + 'BTT_DOWN + ' + Me.BTT_DOWN.Image = CType(resources.GetObject("BTT_DOWN.Image"), System.Drawing.Image) + Me.BTT_DOWN.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_DOWN.Name = "BTT_DOWN" + Me.BTT_DOWN.Size = New System.Drawing.Size(81, 22) + Me.BTT_DOWN.Text = "Download" + Me.BTT_DOWN.ToolTipText = "Download pending items" + ' + 'BTT_STOP + ' + Me.BTT_STOP.AutoToolTip = False + Me.BTT_STOP.Image = CType(resources.GetObject("BTT_STOP.Image"), System.Drawing.Image) + Me.BTT_STOP.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_STOP.Name = "BTT_STOP" + Me.BTT_STOP.Size = New System.Drawing.Size(51, 22) + Me.BTT_STOP.Text = "Stop" + ' + 'BTT_DELETE + ' + Me.BTT_DELETE.Image = CType(resources.GetObject("BTT_DELETE.Image"), System.Drawing.Image) + Me.BTT_DELETE.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_DELETE.Name = "BTT_DELETE" + Me.BTT_DELETE.Size = New System.Drawing.Size(60, 22) + Me.BTT_DELETE.Text = "Delete" + Me.BTT_DELETE.ToolTipText = "Delete selected items" + ' + 'BTT_CLEAR_DONE + ' + Me.BTT_CLEAR_DONE.Image = CType(resources.GetObject("BTT_CLEAR_DONE.Image"), System.Drawing.Image) + Me.BTT_CLEAR_DONE.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_CLEAR_DONE.Name = "BTT_CLEAR_DONE" + Me.BTT_CLEAR_DONE.Size = New System.Drawing.Size(54, 22) + Me.BTT_CLEAR_DONE.Text = "Clear" + Me.BTT_CLEAR_DONE.ToolTipText = "Remove all downloaded items" + ' + 'BTT_CLEAR_ALL + ' + Me.BTT_CLEAR_ALL.Image = CType(resources.GetObject("BTT_CLEAR_ALL.Image"), System.Drawing.Image) + Me.BTT_CLEAR_ALL.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_CLEAR_ALL.Name = "BTT_CLEAR_ALL" + Me.BTT_CLEAR_ALL.Size = New System.Drawing.Size(69, 22) + Me.BTT_CLEAR_ALL.Text = "Clear all" + Me.BTT_CLEAR_ALL.ToolTipText = "Remove all items (pending and downloaded)" + ' + 'SEP_LOG + ' + Me.SEP_LOG.Name = "SEP_LOG" + Me.SEP_LOG.Size = New System.Drawing.Size(6, 25) + ' + 'BTT_LOG + ' + Me.BTT_LOG.AutoToolTip = False + Me.BTT_LOG.Image = CType(resources.GetObject("BTT_LOG.Image"), System.Drawing.Image) + Me.BTT_LOG.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_LOG.Name = "BTT_LOG" + Me.BTT_LOG.Size = New System.Drawing.Size(50, 22) + Me.BTT_LOG.Text = "LOG" + ' + 'BTT_INFO + ' + Me.BTT_INFO.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right + Me.BTT_INFO.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image + Me.BTT_INFO.Image = Global.SCrawler.My.Resources.Resources.InfoPic_32 + Me.BTT_INFO.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_INFO.Name = "BTT_INFO" + Me.BTT_INFO.Size = New System.Drawing.Size(23, 22) + Me.BTT_INFO.ToolTipText = "Show program information and check for updates" + ' + 'BTT_DONATE + ' + Me.BTT_DONATE.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right + Me.BTT_DONATE.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image + Me.BTT_DONATE.Image = Global.SCrawler.My.Resources.Resources.HeartPic_32 + Me.BTT_DONATE.ImageTransparentColor = System.Drawing.Color.Magenta + Me.BTT_DONATE.Name = "BTT_DONATE" + Me.BTT_DONATE.Size = New System.Drawing.Size(23, 22) + Me.BTT_DONATE.ToolTipText = "Support" + ' + 'VideoListForm + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(584, 461) + Me.Controls.Add(Me.TP_CONTROLS) + Me.Controls.Add(Me.TOOLBAR_TOP) + Me.Controls.Add(Me.TOOLBAR_BOTTOM) + Me.Icon = Global.SCrawler.My.Resources.SiteYouTube.YouTubeIcon_32 + Me.KeyPreview = True + Me.MinimumSize = New System.Drawing.Size(300, 200) + Me.Name = "VideoListForm" + Me.Text = "YouTube Downloader" + Me.TOOLBAR_BOTTOM.ResumeLayout(False) + Me.TOOLBAR_BOTTOM.PerformLayout() + Me.TOOLBAR_TOP.ResumeLayout(False) + Me.TOOLBAR_TOP.PerformLayout() + Me.ResumeLayout(False) + Me.PerformLayout() + + End Sub + + Private WithEvents TOOLBAR_BOTTOM As StatusStrip + Private WithEvents PR_MAIN As ToolStripProgressBar + Private WithEvents LBL_INFO As ToolStripStatusLabel + Protected WithEvents TP_CONTROLS As TableLayoutPanel + Private WithEvents TOOLBAR_TOP As ToolStrip + Private WithEvents BTT_DELETE As ToolStripButton + Private WithEvents BTT_CLEAR_DONE As ToolStripButton + Private WithEvents BTT_CLEAR_ALL As ToolStripButton + Private WithEvents BTT_SETTINGS As ToolStripButton + Private WithEvents SEP_1 As ToolStripSeparator + Private WithEvents SEP_LOG As ToolStripSeparator + Private WithEvents BTT_LOG As ToolStripButton + Private WithEvents BTT_STOP As ToolStripButton + Private WithEvents BTT_INFO As ToolStripButton + Private WithEvents BTT_DONATE As ToolStripButton + Protected WithEvents BTT_ADD As PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick + Protected WithEvents BTT_ADD_PLS_ARR As PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick + Protected WithEvents BTT_ADD_NO_SHORTS As PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick + Protected WithEvents BTT_ADD_SHORTS_ONLY As PersonalUtilities.Forms.Controls.KeyClick.ToolStripMenuItemKeyClick + Protected WithEvents MENU_ADD As ToolStripDropDownButton + Protected WithEvents BTT_DOWN As ToolStripButton + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/VideoListForm.resx b/SCrawler.YouTube/Downloader/VideoListForm.resx new file mode 100644 index 0000000..18d3f3c --- /dev/null +++ b/SCrawler.YouTube/Downloader/VideoListForm.resx @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + False + + + 17, 17 + + + 177, 17 + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAN7SURBVEhLrZVJTFNRFIafQhgkQA1OZYriSI0UtUI0vIKg + UEGNBRSUIQ4MihElUAgOqeKwcGM07owLYoxxYzSuHBZIjAoKrfpaoBZLJyiaYNxf83vus0QWBAy+k/xp + k3vzf+ee99/3hNkq7GZIZ/itEEwnvhbcNvfiRmlWDcu1iNj5UYRBItlE7HflyZDgtrkXN9n4ScMWdAuI + eSsg7r2AhL5Q1PoKlQPoJA2LfSdg8ft5WG4JR7oUg9ZAiXKATALwzlOs4dhkj0XO4FJc+V6pHECU0liK + NQJb7CoUOOJp/qtwc6JOOcBRScd0NhUKHQk4NLIWde503P3ZqBygWcpihi+JqHKnosGzCc3ebbj/0/Rv + gJlyPqkjluxfle51OOXRoW1UxKVAPjp/tGDRg8gZpX4U1Sl3mDeZccp30aCIYkcuql0FOOXdi3b/flwN + VNN/Hc769egIFODyuAGdEy24N2GSQXcmGnHjew06xivQ6i+lERahwV0G9eMoyABuHvOGMt4jINESBi3F + kCfFOJyCI+71OO3NxLnR7WRgwLXxQhlwaSyfzETUedJxYHg18oaWQWuLQVJ/GFTkU2Yz/gXwzrk5X9Ta + ommzGiVB8zO+TJz35+JywECAfFwYy4XJn0XPYjMOj2iwz7kC2YNLsEGKRrwlFKpeAZF0KUs+TgEU2UUk + k3k6dZDnUMsxrPVo0eTLwNnRHJwn03Z/Nky+LJzxbkE9rVW5NdjjXA49nVQ27w8Fv4zcfN5zAcbeKYCD + jh3Yao+jGCaiwrUOjfQwWygpbWTITZt922Q10ajqaSQ8TbudycgaWILUz1FQfwhBNI04okuA8Iz0mACv + pwBOSBnMNKBnZmcVMw8fY+av9czsqqPf4+yi/SS7/e0EmWeg1q1F+cgaFDqT0OatRtmHHb9qpGxWKYms + nKQniZ9IfaQekckAHtPpIjYpvun6QBur9aSh3LUau+iUmXTaBk+5vDajeExnK76xw97EDtBzKRhKwGa6 + 0SutkTjsKpZNgtvmXtzELDWw/KF4+UXH36ZxlJRDjuCM/7f+AGrYRlssEikpCynO/NtQOpnz/y1u0ihV + sKT+cKgohgteCQh5QSmxKgg4KukZN4+gzufzGD4lwDsFARUUv0jqXKALJDwhPSRAt4KA0s9GeSTGHhJd + IGMX6aVSAMoyN5pWs+ZcEH4DgcGuQfDpaFIAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAN6SURBVEhLrZVbSJNhGMe/UjyiLixrnig7J7WVphXOLa25 + tKKlluYBy1MZWaJLNGNkh6sgiu6iC4mIbsLoqsNFRZRa6bRva1vW3MlmgeH9G/+e92OSF6Jh3wN/Nnhf + /r/nfb7/+33CfBV2M6Qn/FYIZhNfC25beHEjtSWd5Q1psHdYA4NIsmpQ6sqXIMFtCy9ukjGSzqJeC4h9 + JyD+vYCkj6Go9xXKB8gS01lcn4Bl7xdh5VA41GIszgeK5QPsJADvPM0Sjm22OOjsy3HlZ6V8AK2oZmmW + CGy3KVDgTKT5r8HNyQb5ACfELJZpVaDQmYRjY+vR4Fbj7lSzfIBWMZcZviSjyr0RTZ5taPXuwv0p078B + 5sr5tGqHdb8r3RtwxpOJ9nENLgX06PnVhqUPIueU8lF0j9Rh/nTGKd9Fdg0OO/NQ7SrAGe9BdPhLcTVQ + Tf8z0enPRXegAJcnDOiZbMO9SZMEujPZjBs/69A9UYHz/hIaYRGa3Eeh7I2GBODmsW8p4/0CkofCoKIY + 8qQYv6bhuDsdZ73ZuDC+mwwMuDZRKAEufdeTmQYNHjWOfF2LfMcKqKyxSBkMg4J8jlqNfwG8c27OF1XW + GNqsRHHQ/JwvG13+PFwOGAigx8XveTD5c+hZZKBmbBMOja6C1p6AzWIMEodCoRgQEEmXsnh4BqDIpkEq + maupg3ynUophvUeFFl8WOsd16CLTDr8WJl8Oznm3o5HWqtybcGB0JXLppJL5YCj4ZeTmi54JMA7MAJQ7 + 92CnLZ5imIwK1wY008Nso6S0kyE3bfXtktRCo2qkkfA07R9NRc7nBGz8FA3lhxDE0IgjXgoQnpJ6CfBm + BuCUbQczOXTM/K2KmV21zDzWyMzuBvo9ybrtp9ntH6fIPAv1bhXKxtahcDQF7d5qlA/qf9eJu1mlqGVl + JB1JO0L6SOrXMgnAYzpbxKbFN113tLN6zxaUudZiH50ym07b5CmT1uYUj+l8xTdesbewI/RcChxJyKAb + vdoSiRrXYckkuG3hxU3M1iamdyRKLzr+No2npBxzBmf8vyUBxDq21RqHZErKEooz/zaUTOf8f4ubNIsV + LGUwHAqKYdQrASHPKSUWGQEnRB3j5hHU+WIewycE6JMRUEHxi6TOBbpAwmPSQwK8lhFQ8skojcTYT6IL + ZHxJeiEXgLLMjWbVvDkXhD8Iya6ZQXWVtAAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAOBSURBVEhLrZVbSJNhGMe/Ujwk6sJO8xBlJy1qljapnKUr + nd8qmlqtPNDBQxmtRJd0IrKSbiKKoIvoQiKimyi66nBREZWabtVmbk12tplheP/Gv+f92MgL0bDvgT8M + 3pf/73mf9/9+E6aqmBtRnbE3ozCR+Fp42/SLG22yqlmxRYNtnzTQ2Uh2DXa7tRIkvG36xU0KP6vZrDcC + kt4LSOkRkNYbjfqAKB+gyKZmyR8EzO2ZgUWWWOTYknAqVCEfYCsBeOeZ1lis60/GloH5uDxSLR9AtG1i + mdY4rO9XoNSZSvNfihujDfIBDn0tYnl2BURnGvZ7VqDBm4O7Yyb5AC2uMqb7lo4abzaafOvQ4t+I+2Pm + fwNMlvOITC7972pvFo778tA2pMHFUAk6f7VizoP4SaV8lNApdaiNZJzyrR/QoNxZjFp3KY77d+J0cDeu + hGrpdx7OBAvRHirFpWEdOkdbcW/ULIHujJpwfaQO7cNVOBWspBHq0eTdC+XjBEgAbp70jjLeJSDdEgMV + xZAnxTCYiYPeVTjhz8fZoSIy0KFjWJQAF7+XkJkGDb4c7BlcBq1jAVT2JGT0xUBBPnvthr8A3jk354sq + eyJtVqIibH4ykI9zwWJcCukIUILz34thDhbQXeTigGcldrkWY/PAPKy2JSLVEg1Ft4B4epQVn8YB9P0a + LCTzHOpA61RKMaz3qdAcUOPM0BacI9PTwc0wBwpw0r8ejbRW412JHa5FKKSTSuZ90eCPkZvPeC7A0D0O + sM+5FRv6UyiG6ahyZ8FEl9lKSWkjQ27aEtgoqZlG1Ugj4Wna7lqIgq/zkP0lAcqPUUikEce9EiA8Iz0m + wNtxgKNeLTMH9OzCcA1r/3GYdYw0so6fDezqyBF2zX+M3fpxlMzVqPeqYPQsh+jKQJu/FnUOw+8613ZW + 7RCZ0SYyPUn8TOoldYlMAvCYThSxiPim24E2Vu9bA6N7GcrolPl02iafUVqbVDymUxXfeN3fzPbQvZQ6 + 0pBLL3qJNR4H3OWSSXjb9IubXPY0sRJHqvSh41/TFErKfmd4xv9b3OTCYB1ba09GOiVlNsWZ/zdURnL+ + v8VNTM4qltEXCwXFcNZrAVEvKCVWGQGH7HrGzeOo85k8hk8J8EFGQBXFL546F+gBCU9IDwnwRkZA5ReD + NBJDF4kekOEV6aVcAMoyN5pQU+ZcEP4ATUiw5fkSx60AAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAOBSURBVEhLrZVbSJNhGMe/Ujwk6sJO8xBlJy1qljapnKUr + nd8qmlqtPNDBQxmtRJd0IrKSbiKKoIvoQiKimyi66nBREZWabtVmbk12tplheP/Gv+f92MgL0bDvgT8M + 3pf/73mf9/9+E6aqmBtRnbE3ozCR+Fp42/SLG22yqlmxRYNtnzTQ2Uh2DXa7tRIkvG36xU0KP6vZrDcC + kt4LSOkRkNYbjfqAKB+gyKZmyR8EzO2ZgUWWWOTYknAqVCEfYCsBeOeZ1lis60/GloH5uDxSLR9AtG1i + mdY4rO9XoNSZSvNfihujDfIBDn0tYnl2BURnGvZ7VqDBm4O7Yyb5AC2uMqb7lo4abzaafOvQ4t+I+2Pm + fwNMlvOITC7972pvFo778tA2pMHFUAk6f7VizoP4SaV8lNApdaiNZJzyrR/QoNxZjFp3KY77d+J0cDeu + hGrpdx7OBAvRHirFpWEdOkdbcW/ULIHujJpwfaQO7cNVOBWspBHq0eTdC+XjBEgAbp70jjLeJSDdEgMV + xZAnxTCYiYPeVTjhz8fZoSIy0KFjWJQAF7+XkJkGDb4c7BlcBq1jAVT2JGT0xUBBPnvthr8A3jk354sq + eyJtVqIibH4ykI9zwWJcCukIUILz34thDhbQXeTigGcldrkWY/PAPKy2JSLVEg1Ft4B4epQVn8YB9P0a + LCTzHOpA61RKMaz3qdAcUOPM0BacI9PTwc0wBwpw0r8ejbRW412JHa5FKKSTSuZ90eCPkZvPeC7A0D0O + sM+5FRv6UyiG6ahyZ8FEl9lKSWkjQ27aEtgoqZlG1Ugj4Wna7lqIgq/zkP0lAcqPUUikEce9EiA8Iz0m + wNtxgKNeLTMH9OzCcA1r/3GYdYw0so6fDezqyBF2zX+M3fpxlMzVqPeqYPQsh+jKQJu/FnUOw+8613ZW + 7RCZ0SYyPUn8TOoldYlMAvCYThSxiPim24E2Vu9bA6N7GcrolPl02iafUVqbVDymUxXfeN3fzPbQvZQ6 + 0pBLL3qJNR4H3OWSSXjb9IubXPY0sRJHqvSh41/TFErKfmd4xv9b3OTCYB1ba09GOiVlNsWZ/zdURnL+ + v8VNTM4qltEXCwXFcNZrAVEvKCVWGQGH7HrGzeOo85k8hk8J8EFGQBXFL546F+gBCU9IDwnwRkZA5ReD + NBJDF4kekOEV6aVcAMoyN5pQU+ZcEP4ATUiw5fkSx60AAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAN/SURBVEhLrZVbSJNhGMc/Uzwk6sJO80RZdqRm5ZTElc3S + bVa01LQ8YOWhDE3RKZ0INesmiiK6iS4kIroJo6sOFxZRWalTtzXnYu5kU8Ho/o1/z/s1yQvR0O+BPwze + h//vfZ/3/34T5qvgO4GdIXcDMZv4mr9t4cWN0o1Kpu5X4cCAChoTyaxCgSNLhPjbFl7cRDWoZEvfCYj8 + KCD6i4DY3iBUenTSATJNShb1ScCKLwFY0x+CZFMkmn150gGyCMB3nmgMwU5LFDKtq3BtskQ6gNaUzhKN + oVBaZMixxdD81+POVJV0gFOWTJZilkFni8WJ0Y2ocibj4a866QCNIxqmGYlDqXMzalw70ehOx+Nfhv8D + zJXzadWO6H6XODeh1pWCljEVWn3Z6PzZhOVPwuaU/Fl4599LnM445TvXqsJRmxpljhzUug/jgrcAHb4y + +p2Ci949aPPloH1cg86pJjyaMoigB1N1uD1ZgbbxYjR782mEuahxFkLeFQ4RwM0jP1DGewTE9QdDQTHk + SdF/T8RJ51acd6fh0tg+MtDg+rhOBLT+yCYzFapcyTj2PQlZw6uhMEcivi8YMvIpNOv/AfjOuTlfVJgj + qFmOPL95vScNl71qtPs0BMjGlR9qGLwZdBe7UD66BUfsa7HXuhLbTBGI6Q+C7LOAMHqUeQMzALkWFRLI + PJl2kGWTizGsdCnQ4EnFxbFMXCbTC969MHgyUO9WoprWSp1bcMi+BnvopKJ5XxD4Y+TmAa8E6D/PABy3 + 7cduSzTFMA7Fjk2oo8tsoqS0kCE3bfSki2qgUVXTSHiaDtoTkPFtJTYPhUP+NRARNOLQbgHCS1IXAd7P + AJwdVTODW8eu+kpZ6/hp1jFRzTomq9iNiTPspuscuzdxlsxTUelUoGh0A3T2eLS4y3DaeuR3xUguK7Fq + WZFJy3Qk7SCpl9SjZSKAx3S2iE2LN913t7BK13YUOZKgpVOm0WlrXEXi2pziMZ2veOMtVwM7RveSMxyL + XfSi1xnDUO44Kpr42xZe3KTdUcOyh2PEDx3/mkZTUk7Y/DNebHGTq/YKtsMchThKyjKKM/9vyJ/O+WKL + m9QNF7P4vhDIKIZL3woIfE0pMUoIOGXSMW4eSjtfwmP4ggCfJAQUU/zCaOcCPSDhOekpAd5JCMgf0osj + 0feQ6AHpu0lvpAJQlrnRrJo354LwB0sEsKr2elKBAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAN7SURBVEhLrZVJTFNRFIafQhgkQA1OZYriSI0UtUI0vIKg + UEGNBRSUIQ4MihElUAgOqeKwcGM07owLYoxxYzSuHBZIjAoKrfpaoBZLJyiaYNxf83vus0QWBAy+k/xp + k3vzf+ee99/3hNkq7GZIZ/itEEwnvhbcNvfiRmlWDcu1iNj5UYRBItlE7HflyZDgtrkXN9n4ScMWdAuI + eSsg7r2AhL5Q1PoKlQPoJA2LfSdg8ft5WG4JR7oUg9ZAiXKATALwzlOs4dhkj0XO4FJc+V6pHECU0liK + NQJb7CoUOOJp/qtwc6JOOcBRScd0NhUKHQk4NLIWde503P3ZqBygWcpihi+JqHKnosGzCc3ebbj/0/Rv + gJlyPqkjluxfle51OOXRoW1UxKVAPjp/tGDRg8gZpX4U1Sl3mDeZccp30aCIYkcuql0FOOXdi3b/flwN + VNN/Hc769egIFODyuAGdEy24N2GSQXcmGnHjew06xivQ6i+lERahwV0G9eMoyABuHvOGMt4jINESBi3F + kCfFOJyCI+71OO3NxLnR7WRgwLXxQhlwaSyfzETUedJxYHg18oaWQWuLQVJ/GFTkU2Yz/gXwzrk5X9Ta + ommzGiVB8zO+TJz35+JywECAfFwYy4XJn0XPYjMOj2iwz7kC2YNLsEGKRrwlFKpeAZF0KUs+TgEU2UUk + k3k6dZDnUMsxrPVo0eTLwNnRHJwn03Z/Nky+LJzxbkE9rVW5NdjjXA49nVQ27w8Fv4zcfN5zAcbeKYCD + jh3Yao+jGCaiwrUOjfQwWygpbWTITZt922Q10ajqaSQ8TbudycgaWILUz1FQfwhBNI04okuA8Iz0mACv + pwBOSBnMNKBnZmcVMw8fY+av9czsqqPf4+yi/SS7/e0EmWeg1q1F+cgaFDqT0OatRtmHHb9qpGxWKYms + nKQniZ9IfaQekckAHtPpIjYpvun6QBur9aSh3LUau+iUmXTaBk+5vDajeExnK76xw97EDtBzKRhKwGa6 + 0SutkTjsKpZNgtvmXtzELDWw/KF4+UXH36ZxlJRDjuCM/7f+AGrYRlssEikpCynO/NtQOpnz/y1u0ihV + sKT+cKgohgteCQh5QSmxKgg4KukZN4+gzufzGD4lwDsFARUUv0jqXKALJDwhPSRAt4KA0s9GeSTGHhJd + IGMX6aVSAMoyN5pWs+ZcEH4DgcGuQfDpaFIAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVDSURBVEhLjZVrTJNXGMcLQmdHO6AdarLSOcQBAgX61tK6 + qTAuUrRgC4KOETWj4gqKF5QoRmM00SgmS/Zh+7B92DKTGbdEl2VjwiibCmTKAKdbuQ5rKb0XXnZJFujZ + /5RWZywbT/JL+57znP/z73NOz8uh0V9QEGVMSPiwc8WK4RsSyQebxeKXMBzhn/yfGFIolL9JJDdHli/v + u5aYWI6hKBDmn6Rx32Dg3yko+HyoqYmMX7pE7jU2+m4olR3ZAsFqTEfOZ4UOE8O8bt2x4yF74QKZaWkh + wxUV7veSk+sk0dGxmJ4v0rFq1fuDhw6RsdOnyfipU8SCZPPRo6RVqbwpFQheRUrIIiMq1RsQN7MXLxIW + a9nmZjJz8iQZ1Gg8X4rF7yJFCCI4nSKRhYqPNTaSh8ePEwuSJs+dI6amJt8NheJWukCQhMSniqAthdbK + Sgt1TsVnIM4eOUJmYMxbXU2McXEPkKYEAk57fPzl4ZoaMo4CZmA5doxYscB+9iwZQrvalcruND4/Gcn+ + IkMyWeFkRYVl+vx5wsLMDEyxWDdz8CCZ2ruXtCoUzp0i0VWkFgMhR7dy5cutcvnNgd27fY+QNAEm4caO + PXGhZY7Dh0knimTx+Sk/MUzBRHm5dfrMGX9LWDhm0V62oYFMQ9yYleVO4vE+gbAeSAHdcE4kIxIlo0i3 + eedOYt23j9jq64n9wAHiRDEPhEbr6309KtXAaGmphT1xwu+YxRyLXLau7rF4Co93GXo1IDUgHg78wc0S + ClO+lcm6fq2q8tlqa4kDuAwG4oaAF8W8+/eTabidpsJ4ZvHsF0d7u+Ryj5TPp+LU+RqwFDw5qoHgKoTC + Ne0ZGd3WbduIY9cu4gIevZ544XAKBaewgVOlpcSbm0u8KhVxg67sbG+mQHAF6/cA6pwHnhEPBndtbGxq + u1TaPZibO+vevp241GriYhjiFIuJUyAgzshI4lyyhIzy+b62hASPMiaGbmgtSAPPgwXFg+Fv1x2ptH8S + gjaI2YEDUGE/ERGkLyPjr/Lk5K+R/w5IB4sS94e1utpgVanMNh7vWXFAx0yJibNGjWY0JT6+FEuiweLE + vXr9YWdentMG9wuJT4CHwJyaSvq02gdvrluXiaX/ea34w6XXNzs2bXLZoqKeEXeg97RdVHwcDIeHk/ug + NzPT119W9ku5UknbtHARz549x+1FRe5Qzh0iEXlUVPT3UFKSj4oPBcVBF+hmmLl++ksWKoKj2GzLy/PY + +Hy/8FNtWbaMmIqLfz+zYUNP75Ytk+aUFPIzRO+C2+AHYAS31q6d+7G8fCBPKqUX5JOr3l1T00TF7aHE + 4+KIaePGP+oYph2p9UVpaW89KCkZNaH3QfEO0AZaQadCMddTVtb7sUZDN54LwjjurVst9piY0M4hXiuT + tSHRAOhGCg/k5LzWp9ONDKSnPxb/BnwFroN+mWz2rk43iNyVgMuxabUmt0QS0rmBYb5DUl1APPim4h7M + z1f2lZWNfJ+e7vu3+BdhYeRuVtasUa2eQF4BiOX0VFXtmFSrXS6IBp3fLyz8c19o8WBwG3JysrGxw3cy + Msi1gLgxO3vuM7X6UfHq1R8hZ74AYmlbRcUJS0mJZ0yp9FHnDXJ5B8YXEg8GV79+vYy2A9e5z6hSzV2F + 8xd4vBbMacF8ixBhTEJCdK9O9+lYSYnjSn4+Tl94A8YZwKfzNGmB4F6vrHz7nlY7cVujcRYlJdF3gQ6I + AT2uj9fSLzFADjYHPhf7938O0KNJT84W8AoI+YdbAqhj+rKmn/R5MUFN0Pv/xQC0YMAYh/MP1UTZ10sP + VAUAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVDSURBVEhLjZVtTFNXGMcLQmdHO6AdarLSOcTxXqC3lls3 + FeVFihZsqaBjRM2ouILiC0oUozGaaHxJluzD9mH7sGUmM26JLsuGwqiZE5jKQAcGAR3WUvpeuOwlWaRn + /1NanbFsPMkvt/ec5/yff59z7r08Gn1FRTHmpKRPri1aNHxVJvt4nVT6GoajApP/E0MqFfubTHZ9ZOHC + 3kvJyQYMxYCIwCSNfpNJeKuo6Kuh5mYyeu4cudnU5L/Ksh15ItFSTEfPZIWPQYZ527Z58yPu9GkydfYs + Ga6s9HyYmlovi42Nx/RMkY4lSz66v3cveXjsGBk9epRYkWw5cIC0sux1uUj0JlLCFhlRq9dA3MKdOUM4 + rOVaWsjUkSPkvlbr/UYq/QApYhDFuyaRWKn4w6Ym8ujQIWJF0vjJk6Svudl/VaX6KUskSkHic0XQlmJb + VZWVOqfiUxDn9u8nUzDmq6kh5oSEAaSxQMRrT0w8P1xbS0ZRwAKsBw8SGxY4Tpwgd9GudpbtyhQKU5Ec + KDKkUBSPV1ZaJ0+dIhzMTMEUh3VTe/aQiR07SKtK5doikVxEaikQ8/SLF7/eqlRe/3nbNv9jJI2Bcbhx + YE/caJlz3z5yDUVyhcK0XximaMxgsE0ePx5oCQfHHNrLNTaSSYibc3M9KQLB5xA2AjmgG86LZiSSVBTp + smzZQmw7dxJ7QwNx7N5NXCjmhdBAQ4O/W62+86C83ModPhxwzGGOQy5XX/9UPE0gOA+9WpARFI8EgeDn + isVpVxSKzt7qar+9ro44gdtkIh4I+FDMt2sXmYTbSSqMew73AXG0t1Op9MqFQipOnaeD+eDZUQ0GXyUW + p7dnZ3fZNm4kzq1biRt4jUbig8MJFJzABk6UlxPf6tXEp1YTD+jMy/PliEQXsH47oM4F4AXxUPCXxcdn + tMvlXf1q9RPPpk3ErdEQN8MQl1RKXCIRcUVHE9e8eWRAKPS3JSV52bg4uqF1IBO8DGYVD0WgXbfk8r5x + CNoh5gBOQIUDREWR3uzsvwypqd8h/32QBeYkHghbTY3JplZb7ALBi+KAjvUnJz8xa7UP0hITy7EkFsxN + 3Gc07nMVFLjscD+b+Bh4BCwZGaRXpxt4Z/nyHCz9z9dKINxGY4tz7Vq3PSbmBXEnek/bRcVHwXBkJOkH + N3Jy/H0VFfcMLEvbNHsR7/bthxwlJZ5wzp0SCXlcUvL33ZQUPxUfCor3gE5gZpjpPvpPZiuCo9hiLyjw + 2oXCgPBzbVmwgAyWlv5+fOXK7p7168ctaWnkV4jeBjfAj7QAZdmy6ZsGw50CuZy+IJ+96j21tc1U3BFO + PCGBDK5a9Uc9w7QjtaEkM/PdgbKyB4PofUi8A7SBVlpEpZrurqjo+UyrpRvPBxE8z4YNVkdcXHjnEK9T + KNqQaAJ0I8W78/Pf6tXrR+5kZT0V/x58Cy6DvvT0J7f1+vvIXQz4PLtON+iRycI6NzHMD0iqD4qHvlT8 + PYWFbG9FxciVrCz/v8W/joggt1HArNGMIa8IxPO6q6s3j2s0bjdEQ877i4v/3BlePBT8xvz8PGzs8K3s + bHIpKG7Oy5v+UqN5XLp06afImSmAmN9WWXnYWlbmvceyfuq8UanswPhs4qHgG1esUNB24OvnN6vV0xfh + /BWB4CzmdGCmRYgIJikptkev/+JhWZnzQmEhTl9kI8YZIKTzNGmW4F+uqnrvrk43dkOrdZWkpNBvgR5I + AT2uT9fSH3FACdYFr3N9/F8C9GjSk7MevAHCPnDzAHVMP9b0Su/nEtQEff+/GoQWDBrj8f4B7pXZMs39 + OqoAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVDSURBVEhLjZVtTFNXGMcLQmdHO6AdarLSOcTxXqC3lls3 + FeVFihZsqaBjRM2ouILiC0oUozGaaHxJluzD9mH7sGUmM26JLsuGwqiZE5jKQAcGAR3WUvpeuOwlWaRn + /1NanbFsPMkvt/ec5/yff59z7r08Gn1FRTHmpKRPri1aNHxVJvt4nVT6GoajApP/E0MqFfubTHZ9ZOHC + 3kvJyQYMxYCIwCSNfpNJeKuo6Kuh5mYyeu4cudnU5L/Ksh15ItFSTEfPZIWPQYZ527Z58yPu9GkydfYs + Ga6s9HyYmlovi42Nx/RMkY4lSz66v3cveXjsGBk9epRYkWw5cIC0sux1uUj0JlLCFhlRq9dA3MKdOUM4 + rOVaWsjUkSPkvlbr/UYq/QApYhDFuyaRWKn4w6Ym8ujQIWJF0vjJk6Svudl/VaX6KUskSkHic0XQlmJb + VZWVOqfiUxDn9u8nUzDmq6kh5oSEAaSxQMRrT0w8P1xbS0ZRwAKsBw8SGxY4Tpwgd9GudpbtyhQKU5Ec + KDKkUBSPV1ZaJ0+dIhzMTMEUh3VTe/aQiR07SKtK5doikVxEaikQ8/SLF7/eqlRe/3nbNv9jJI2Bcbhx + YE/caJlz3z5yDUVyhcK0XximaMxgsE0ePx5oCQfHHNrLNTaSSYibc3M9KQLB5xA2AjmgG86LZiSSVBTp + smzZQmw7dxJ7QwNx7N5NXCjmhdBAQ4O/W62+86C83ModPhxwzGGOQy5XX/9UPE0gOA+9WpARFI8EgeDn + isVpVxSKzt7qar+9ro44gdtkIh4I+FDMt2sXmYTbSSqMew73AXG0t1Op9MqFQipOnaeD+eDZUQ0GXyUW + p7dnZ3fZNm4kzq1biRt4jUbig8MJFJzABk6UlxPf6tXEp1YTD+jMy/PliEQXsH47oM4F4AXxUPCXxcdn + tMvlXf1q9RPPpk3ErdEQN8MQl1RKXCIRcUVHE9e8eWRAKPS3JSV52bg4uqF1IBO8DGYVD0WgXbfk8r5x + CNoh5gBOQIUDREWR3uzsvwypqd8h/32QBeYkHghbTY3JplZb7ALBi+KAjvUnJz8xa7UP0hITy7EkFsxN + 3Gc07nMVFLjscD+b+Bh4BCwZGaRXpxt4Z/nyHCz9z9dKINxGY4tz7Vq3PSbmBXEnek/bRcVHwXBkJOkH + N3Jy/H0VFfcMLEvbNHsR7/bthxwlJZ5wzp0SCXlcUvL33ZQUPxUfCor3gE5gZpjpPvpPZiuCo9hiLyjw + 2oXCgPBzbVmwgAyWlv5+fOXK7p7168ctaWnkV4jeBjfAj7QAZdmy6ZsGw50CuZy+IJ+96j21tc1U3BFO + PCGBDK5a9Uc9w7QjtaEkM/PdgbKyB4PofUi8A7SBVlpEpZrurqjo+UyrpRvPBxE8z4YNVkdcXHjnEK9T + KNqQaAJ0I8W78/Pf6tXrR+5kZT0V/x58Cy6DvvT0J7f1+vvIXQz4PLtON+iRycI6NzHMD0iqD4qHvlT8 + PYWFbG9FxciVrCz/v8W/joggt1HArNGMIa8IxPO6q6s3j2s0bjdEQ877i4v/3BlePBT8xvz8PGzs8K3s + bHIpKG7Oy5v+UqN5XLp06afImSmAmN9WWXnYWlbmvceyfuq8UanswPhs4qHgG1esUNB24OvnN6vV0xfh + /BWB4CzmdGCmRYgIJikptkev/+JhWZnzQmEhTl9kI8YZIKTzNGmW4F+uqnrvrk43dkOrdZWkpNBvgR5I + AT2uT9fSH3FACdYFr3N9/F8C9GjSk7MevAHCPnDzAHVMP9b0Su/nEtQEff+/GoQWDBrj8f4B7pXZMs39 + OqoAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAVDSURBVEhLjZVtTFNXGMcLQmdHO6AdarLSOcTxXqC3lls3 + FeVFihZsqaBjRM2ouILiC0oUozGaaHxJluzD9mH7sGUmM26JLsuGwqiZE5jKQAcGAR3WUvpeuOwlWaRn + /1NanbFsPMkvt/ec5/yff59z7r08Gn1FRTHmpKRPri1aNHxVJvt4nVT6GoajApP/E0MqFfubTHZ9ZOHC + 3kvJyQYMxYCIwCSNfpNJeKuo6Kuh5mYyeu4cudnU5L/Ksh15ItFSTEfPZIWPQYZ527Z58yPu9GkydfYs + Ga6s9HyYmlovi42Nx/RMkY4lSz66v3cveXjsGBk9epRYkWw5cIC0sux1uUj0JlLCFhlRq9dA3MKdOUM4 + rOVaWsjUkSPkvlbr/UYq/QApYhDFuyaRWKn4w6Ym8ujQIWJF0vjJk6Svudl/VaX6KUskSkHic0XQlmJb + VZWVOqfiUxDn9u8nUzDmq6kh5oSEAaSxQMRrT0w8P1xbS0ZRwAKsBw8SGxY4Tpwgd9GudpbtyhQKU5Ec + KDKkUBSPV1ZaJ0+dIhzMTMEUh3VTe/aQiR07SKtK5doikVxEaikQ8/SLF7/eqlRe/3nbNv9jJI2Bcbhx + YE/caJlz3z5yDUVyhcK0XximaMxgsE0ePx5oCQfHHNrLNTaSSYibc3M9KQLB5xA2AjmgG86LZiSSVBTp + smzZQmw7dxJ7QwNx7N5NXCjmhdBAQ4O/W62+86C83ModPhxwzGGOQy5XX/9UPE0gOA+9WpARFI8EgeDn + isVpVxSKzt7qar+9ro44gdtkIh4I+FDMt2sXmYTbSSqMew73AXG0t1Op9MqFQipOnaeD+eDZUQ0GXyUW + p7dnZ3fZNm4kzq1biRt4jUbig8MJFJzABk6UlxPf6tXEp1YTD+jMy/PliEQXsH47oM4F4AXxUPCXxcdn + tMvlXf1q9RPPpk3ErdEQN8MQl1RKXCIRcUVHE9e8eWRAKPS3JSV52bg4uqF1IBO8DGYVD0WgXbfk8r5x + CNoh5gBOQIUDREWR3uzsvwypqd8h/32QBeYkHghbTY3JplZb7ALBi+KAjvUnJz8xa7UP0hITy7EkFsxN + 3Gc07nMVFLjscD+b+Bh4BCwZGaRXpxt4Z/nyHCz9z9dKINxGY4tz7Vq3PSbmBXEnek/bRcVHwXBkJOkH + N3Jy/H0VFfcMLEvbNHsR7/bthxwlJZ5wzp0SCXlcUvL33ZQUPxUfCor3gE5gZpjpPvpPZiuCo9hiLyjw + 2oXCgPBzbVmwgAyWlv5+fOXK7p7168ctaWnkV4jeBjfAj7QAZdmy6ZsGw50CuZy+IJ+96j21tc1U3BFO + PCGBDK5a9Uc9w7QjtaEkM/PdgbKyB4PofUi8A7SBVlpEpZrurqjo+UyrpRvPBxE8z4YNVkdcXHjnEK9T + KNqQaAJ0I8W78/Pf6tXrR+5kZT0V/x58Cy6DvvT0J7f1+vvIXQz4PLtON+iRycI6NzHMD0iqD4qHvlT8 + PYWFbG9FxciVrCz/v8W/joggt1HArNGMIa8IxPO6q6s3j2s0bjdEQ877i4v/3BlePBT8xvz8PGzs8K3s + bHIpKG7Oy5v+UqN5XLp06afImSmAmN9WWXnYWlbmvceyfuq8UanswPhs4qHgG1esUNB24OvnN6vV0xfh + /BWB4CzmdGCmRYgIJikptkev/+JhWZnzQmEhTl9kI8YZIKTzNGmW4F+uqnrvrk43dkOrdZWkpNBvgR5I + AT2uT9fSH3FACdYFr3N9/F8C9GjSk7MevAHCPnDzAHVMP9b0Su/nEtQEff+/GoQWDBrj8f4B7pXZMs39 + OqoAAAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFmSURBVFhH1dc/K4VhHMbxJ5EFEQbFiERKCotIrMJIiYEi + pbwCZcOqJC9AikUWiqRkJYtSRDbESMT3V07dna7zHHru+9T51me+Ts//E+V7LRjFFAZRiZzUhDVc4/vX + B47Rh6D14Aqp4XQ36ECQ2nALNezaQjG8Vo5DqMF0bxiA1+bwCTWoLMFbNTiDGsrkABXw0jDsKldDmdyj + HokrwCrUSBz7wXbRJs4eLkdQI9m0I3ENeIAaiGN3QjMSZ4fxv+ffnKIKibOnmhqI84V5eMleOHY41VAm + 9k7wdgtW4wRqSHlCP7y2AjWmbMB7Y7DzqgZdz2iF9zrxCDXq2oU9uLz31+tgAcHahhp1DSFY9pGhRl29 + CFYXxrMoQ7BmsZfFPkoRpHWow+56hX26BWkRatR1gRIEaQLvUMMpOyhCkBpxBzWcMoOgLUMNm0vUIWj2 + ebaJF7jj5+hGTiqE/f+bxDRGUIt8LIp+AC/GHt3tQnwvAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/SCrawler.YouTube/Downloader/VideoListForm.vb b/SCrawler.YouTube/Downloader/VideoListForm.vb new file mode 100644 index 0000000..94503a1 --- /dev/null +++ b/SCrawler.YouTube/Downloader/VideoListForm.vb @@ -0,0 +1,499 @@ +' 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.ComponentModel +Imports PersonalUtilities.Tools +Imports PersonalUtilities.Forms +Imports PersonalUtilities.Forms.Toolbars +Imports PersonalUtilities.Forms.Controls.KeyClick +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Functions.Messaging +Imports SCrawler.API.YouTube +Imports SCrawler.API.YouTube.Base +Imports SCrawler.API.YouTube.Controls +Imports SCrawler.API.YouTube.Objects +Namespace DownloadObjects.STDownloader + Public Class VideoListForm : Implements IDesignXMLContainer +#Region "Declarations" + Private ReadOnly MyView As FormView + Private ReadOnly MyProgress As MyProgress + Protected WithEvents MyJob As JobThread(Of MediaItem) + Public Property DesignXML As EContainer Implements IDesignXMLContainer.DesignXML + Public Property DesignXMLNodes As String() Implements IDesignXMLContainer.DesignXMLNodes + Public Property DesignXMLNodeName As String Implements IDesignXMLContainer.DesignXMLNodeName + Private ReadOnly ControlsDownloaded As New FPredicate(Of MediaItem)(Function(i) i.MyContainer.MediaState = Plugin.UserMediaStates.Downloaded) + Private ReadOnly ControlsChecked As Predicate(Of MediaItem) = Function(i) i.Checked + Private ReadOnly CNT_PROCESSOR As TableControlsProcessor + Protected AppMode As Boolean = True +#End Region +#Region "Initializer" + Public Sub New() + InitializeComponent() + CNT_PROCESSOR = New TableControlsProcessor(TP_CONTROLS) + MyView = New FormView(Me) + MyProgress = New MyProgress(TOOLBAR_BOTTOM, PR_MAIN, LBL_INFO) + MyJob = New JobThread(Of MediaItem) + End Sub +#End Region +#Region "Form handlers" + Protected Overridable Sub VideoListForm_Load(sender As Object, e As EventArgs) Handles Me.Load + If Not LicenseManager.UsageMode = LicenseUsageMode.Designtime Then + If MyYouTubeSettings Is Nothing Then MyYouTubeSettings = New YouTubeSettings + DesignXML = MyYouTubeSettings.DesignXml + If MyCache Is Nothing Then MyCache = New CacheKeeper(YouTubeFunctions.YouTubeCachePathRoot) + End If + + If AppMode Then + If Now.Month.ValueBetween(6, 8) Then Text = "SCrawler: Happy LGBT Pride Month! :-)" + MyNotificator = New YTNotificator(Me) + MyDownloaderSettings = MyYouTubeSettings + End If + + With MyView : .Import() : .SetFormSize() : End With + BTT_DELETE.Enabled = False + If Not AppMode Then + BTT_SETTINGS.Visible = False + SEP_1.Visible = False + SEP_LOG.Visible = False + BTT_LOG.Visible = False + BTT_INFO.Visible = False + BTT_DONATE.Visible = False + End If + MyProgress.Visible = False + LoadData() + End Sub + Protected Overridable Sub VideoListForm_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing + If Not AppMode Then e.Cancel = True : Hide() + End Sub + Protected Overridable Sub VideoListForm_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed + MyView.Dispose() + MyCache.DisposeIfReady() + If AppMode Then + MyNotificator.Clear() + If Not MyMainLOG.IsEmptyString Then SaveLogToFile() + End If + If Not MyYouTubeSettings Is Nothing Then MyYouTubeSettings.Close() + End Sub + Private Sub VideoListForm_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown + If e.KeyCode = Keys.Insert Then BTT_ADD.PerformClick() : e.Handled = True + End Sub +#End Region +#Region "Refill, save list" + Protected Sub LoadData() + Dim c As List(Of IYouTubeMediaContainer) = LoadData_GetFiles() + If c.ListExists Then + c.Sort(New ContainerDateComparer) + SuspendLayout() + For i% = c.Count - 1 To 0 Step -1 : ControlCreateAndAdd(c(i), True, i = 0) : Next + ResumeLayout(False) + PerformLayout() + End If + End Sub + Protected Overridable Function LoadData_GetFiles() As List(Of IYouTubeMediaContainer) + Try + Dim l As New List(Of IYouTubeMediaContainer) + Dim path As SFile = DownloaderDataFolderYouTube + If path.Exists(SFO.Path, False) Then + Dim files As List(Of SFile) = SFile.GetFiles(path, "*.xml",, EDP.ReturnValue) + If files.Count > 0 Then files.ForEach(Sub(f) l.Add(YouTubeFunctions.CreateContainer(f))) + End If + If l.Count > 0 Then l.RemoveAll(Function(c) c Is Nothing) + If l.Count > 0 Then l.ListDisposeRemoveAll(Function(c) Not c.Exists) + Return l + Catch ex As Exception + Dim e As EDP = EDP.LogMessageValue + If Not ex.HelpLink.IsEmptyString AndAlso ex.HelpLink = NameOf(YouTubeFunctions.CreateContainer) Then e = EDP.SendToLog + EDP.ReturnValue + Return ErrorsDescriber.Execute(e, ex, "VideoListForm.LoadData_GetFiles", New List(Of IYouTubeMediaContainer)) + End Try + End Function +#End Region +#Region "Controls" + Protected Sub ControlCreateAndAdd(ByVal Container As IYouTubeMediaContainer, Optional ByVal DisableDownload As Boolean = False, + Optional ByVal PerformClick As Boolean = True) + ControlInvokeFast(TP_CONTROLS, Sub() + With TP_CONTROLS + .SuspendLayout() + If DisableDownload Or Not MyDownloaderSettings.DownloadAutomatically Then Container.Save() + '.AutoScroll = True + '.HorizontalScroll.Visible = False + .RowStyles.Insert(0, New RowStyle(SizeType.Absolute, 60)) + .RowCount = .RowStyles.Count + OffsetControls(0, True) + Dim cnt As New MediaItem(Container) With {.Dock = DockStyle.Fill, .Margin = New Padding(0)} + AddHandler cnt.FileDownloaded, AddressOf MediaControl_FileDownloaded + AddHandler cnt.Removal, AddressOf MediaControl_Removal + AddHandler cnt.DownloadAgain, AddressOf MediaControl_DownloadAgain + AddHandler cnt.DownloadRequested, AddressOf MediaControl_DownloadRequested + AddHandler cnt.CheckedChanged, AddressOf MediaControl_CheckedChanged + AddHandler cnt.Click, AddressOf CNT_PROCESSOR.MediaItem_Click + AddHandler cnt.KeyDown, AddressOf CNT_PROCESSOR.MediaItem_KeyDown + .Controls.Add(cnt, 0, 0) + .Controls.Cast(Of ISupportInitialize).ToList.ForEach(Sub(_cnt) _cnt.EndInit()) + .ScrollControlIntoView(cnt) + cnt.Select() + RefillColors() + '.AutoScroll = False + '.AutoScroll = True + .ResumeLayout() + .PerformLayout() + UpdateScrolls(Me, Nothing) + If PerformClick Then cnt.PerformClick() + If Not DisableDownload And MyDownloaderSettings.DownloadAutomatically Then AddToDownload(cnt, True) + End With + End Sub, EDP.None) + End Sub +#Region "Controls rendering" + Private Overloads Sub OffsetControls() + Try + With TP_CONTROLS + If .Controls.Count > 0 Then + Dim i%, ri% + Dim cntIndx% = -1 + Dim cnt As Control + For i = .Controls.Count - 1 To 0 Step -1 + cnt = .Controls(i) + If Not cnt Is Nothing Then cntIndx += 1 : .SetCellPosition(cnt, New TableLayoutPanelCellPosition(0, cntIndx)) + Next + For i = .RowStyles.Count - 1 To 0 Step -1 + If Not .GetControlFromPosition(0, i) Is Nothing Then + If i + 1 < .RowStyles.Count - 1 Then + For ri = .RowStyles.Count - 1 To i + 1 Step -1 : .RowStyles.RemoveAt(i) : Next + .RowStyles.Add(New RowStyle(SizeType.AutoSize)) + .RowCount = .RowStyles.Count + End If + Exit For + End If + Next + Else + .RowStyles.Clear() + .RowCount = 0 + .RowStyles.Add(New RowStyle(SizeType.AutoSize)) + .RowCount = .RowStyles.Count + End If + End With + Catch + End Try + End Sub + Private Overloads Sub OffsetControls(ByVal ReflectedRow As Integer, ByVal Add As Boolean) + ControlInvokeFast(TP_CONTROLS, Sub() + Dim offset% = IIf(Add, 1, -1) + Dim cnt As Control + With TP_CONTROLS + If .RowStyles.Count > 1 Then + For i% = .RowStyles.Count - 1 To ReflectedRow Step -1 + cnt = .GetControlFromPosition(0, i) + If Not cnt Is Nothing Then .SetCellPosition(cnt, New TableLayoutPanelCellPosition(0, i + offset)) + Next + End If + End With + End Sub, EDP.None) + End Sub + Private Sub RefillColors() + ControlInvokeFast(TP_CONTROLS, Sub() + With TP_CONTROLS + If .Controls.Count > 0 Then + Dim i% = 0 + Dim c As Color + For Each cnt As MediaItem In .Controls + i += 1 + If (i Mod 2) = 0 Then c = SystemColors.ControlLight Else c = SystemColors.Window + cnt.BackColor = c + Next + End If + End With + End Sub, EDP.None) + End Sub + Private Sub UpdateScrolls(sender As Object, e As EventArgs) Handles TP_CONTROLS.StyleChanged, Me.ResizeEnd, Me.SizeChanged + ControlInvokeFast(TP_CONTROLS, Sub() + With TP_CONTROLS + .SuspendLayout() + .Padding = New Padding(0, 0, .VerticalScroll.Visible.BoolToInteger * 3, 0) + .HorizontalScroll.Visible = False + .HorizontalScroll.Enabled = False + .ResumeLayout() + .PerformLayout() + End With + End Sub, EDP.None) + End Sub +#End Region +#Region "Toolbar controls handlers" + Protected Overridable Sub BTT_SETTINGS_Click(sender As Object, e As EventArgs) Handles BTT_SETTINGS.Click + MyYouTubeSettings.ShowForm(AppMode) + End Sub + Protected Overridable Sub BTT_ADD_KeyClick(ByVal Sender As ToolStripMenuItemKeyClick, ByVal e As KeyClickEventArgs) Handles BTT_ADD.KeyClick, BTT_ADD_PLS_ARR.KeyClick, + BTT_ADD_NO_SHORTS.KeyClick, BTT_ADD_SHORTS_ONLY.KeyClick + Dim pForm As ParsingProgressForm = Nothing + Try + Dim canProcess As Boolean = True + If TP_CONTROLS.Controls.Count >= MyYouTubeSettings.ItemsListLimit Then canProcess = TP_CONTROLS.Controls.Cast(Of MediaItem).ListExists(ControlsDownloaded) + If canProcess Then + Dim useCookies As Boolean = MyYouTubeSettings.DefaultUseCookies + If e.Control Then useCookies = True + Dim useCookiesParse As Boolean? = Nothing + If useCookies Then useCookiesParse = True + + Dim c As IYouTubeMediaContainer = Nothing + Dim url$ = String.Empty + Dim GetDefault As Boolean = True + Dim GetShorts As Boolean = True + + If Sender.Tag = "pls" Then + Using pf As New PlaylistArrayForm With {.DesignXML = DesignXML} + pf.ShowDialog() + If pf.DialogResult = DialogResult.OK Then + With pf.URLs + If .Count > 0 Then + pForm = New ParsingProgressForm + pForm.Show() + pForm.SetInitialValues(.Count, "Parsing playlists...") + Dim containers As New List(Of IYouTubeMediaContainer) + For Each u$ In .Self : containers.Add(YouTubeFunctions.Parse(u, useCookiesParse, pForm.Token, pForm.MyProgress, True, False)) : pForm.MyProgress.Perform() : Next + pForm.Dispose() + If containers.Count > 0 Then containers.ListDisposeRemoveAll(Function(cc) cc.HasError Or Not cc.Exists) + If containers.Count > 0 Then + c = New Channel With {.UserTitle = IIf(pf.IsOneArtist, containers(0).UserTitle, "Playlists")} + c.Elements.AddRange(containers) + End If + End If + End With + End If + End Using + Else + Select Case CStr(Sender.Tag) + Case "ans" : GetShorts = False + Case "as" : GetDefault = False : GetShorts = True + End Select + url = BufferText + If url.IsEmptyString OrElse Not YouTubeFunctions.IsMyUrl(url) Then url = InputBoxE("Enter a valid URL to the YouTube video:", "YouTube link") + End If + + If Not c Is Nothing OrElse YouTubeFunctions.IsMyUrl(url) Then + If c Is Nothing Then + pForm = New ParsingProgressForm + pForm.Show() + pForm.SetInitialValues(1, "Parsing data...") + c = YouTubeFunctions.Parse(url, useCookiesParse, pForm.Token, pForm.MyProgress, GetDefault, GetShorts) + pForm.Dispose() + End If + If Not c Is Nothing Then + Dim f As Form + Select Case c.ObjectType + Case YouTubeMediaType.Single : f = New VideoOptionsForm(c) + Case YouTubeMediaType.Channel, YouTubeMediaType.PlayList + If c.IsMusic Then + f = New MusicPlaylistsForm(c) + Else + f = New VideoOptionsForm(c) + End If + Case Else : c.Dispose() : Throw New ArgumentException($"Object type {c.ObjectType} not implemented", "IYouTubeMediaContainer.ObjectType") + End Select + If Not f Is Nothing Then + If TypeOf f Is IDesignXMLContainer Then DirectCast(f, IDesignXMLContainer).DesignXML = DesignXML + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then + If TP_CONTROLS.Controls.Count >= MyYouTubeSettings.ItemsListLimit Then _ + RemoveControls(TP_CONTROLS.Controls.Cast(Of MediaItem).LastOrDefault(ControlsDownloaded)) + ControlCreateAndAdd(c) + End If + f.Dispose() + End If + End If + End If + Else + MsgBoxE({$"Number of items to download exceeded!{vbCr}Reduce the number of items or increase the limit.", "New download"}, vbCritical) + End If + Catch oex As OperationCanceledException + Catch dex As ObjectDisposedException + Catch ex As Exception + ErrorsDescriber.Execute(EDP.LogMessageValue, ex, "VideoListForm.Add") + UpdateLogButton() + Finally + If Not pForm Is Nothing Then pForm.Dispose() + End Try + End Sub + Private Sub BTT_DOWN_Click(sender As Object, e As EventArgs) Handles BTT_DOWN.Click + With TP_CONTROLS + If .Controls.Count > 0 Then + For Each cnt As MediaItem In .Controls + If Not cnt.MyContainer.MediaState = Plugin.UserMediaStates.Downloaded And Not cnt.Pending Then AddToDownload(cnt, False) + Next + End If + End With + StartDownloading() + End Sub + Private Sub BTT_STOP_Click(sender As Object, e As EventArgs) Handles BTT_STOP.Click + ControlInvoke(TOOLBAR_TOP, BTT_STOP, Sub() BTT_STOP.Enabled = False, EDP.SendToLog) + MyJob.Cancel() + End Sub + Private Sub BTT_DELETE_Click(sender As Object, e As EventArgs) Handles BTT_DELETE.Click + RemoveControls(ControlsChecked) + End Sub + Protected Overridable Sub BTT_CLEAR_DONE_Click(sender As Object, e As EventArgs) Handles BTT_CLEAR_DONE.Click + RemoveControls(ControlsDownloaded) + End Sub + Protected Overridable Sub BTT_CLEAR_ALL_Click(sender As Object, e As EventArgs) Handles BTT_CLEAR_ALL.Click + RemoveControls() + End Sub + Private Sub BTT_LOG_Click(sender As Object, e As EventArgs) Handles BTT_LOG.Click + MyMainLOG_ShowForm(DesignXML,,,, AddressOf UpdateLogButton) + End Sub + Friend Sub UpdateLogButton() + If AppMode Then MyMainLOG_UpdateLogButton(BTT_LOG, TOOLBAR_TOP) + End Sub + Private Sub BTT_DONATE_Click(sender As Object, e As EventArgs) Handles BTT_DONATE.Click + Try : Process.Start("https://github.com/AAndyProgram/SCrawler/blob/main/HowToSupport.md") : Catch : End Try + End Sub + Private Sub BTT_INFO_Click(sender As Object, e As EventArgs) Handles BTT_INFO.Click + Try + MsgBoxE({$"YouTube Downloader v{My.Application.Info.Version}" & vbCr & + $"Address: https://github.com/AAndyProgram/SCrawler" & vbCr & + "Created by Greek LGBT person Andy (Gay)", + "Program information"},,,, + {"OK", New MsgBoxButton("Go to site") With {.CallBack = Sub(r, n, b) Process.Start("https://github.com/AAndyProgram/SCrawler/releases")}}) + Catch + End Try + End Sub + Protected Overloads Sub RemoveControls(Optional ByVal Predicate As Predicate(Of MediaItem) = Nothing) + ControlInvokeFast(TP_CONTROLS, Sub() + With TP_CONTROLS + If .Controls.Count > 0 Then + Dim i% + Dim rCnt As New List(Of Integer) + Dim predicateExists As Boolean = Not Predicate Is Nothing + For i = 0 To .Controls.Count - 1 + If Not predicateExists OrElse Predicate.Invoke(.Controls(i)) Then rCnt.Add(i) + Next + If rCnt.Count > 0 Then + Dim cnt As MediaItem + For i = rCnt.Count - 1 To 0 Step -1 + cnt = .Controls(rCnt(i)) + .Controls.RemoveAt(rCnt(i)) + If Not cnt.MyContainer Is Nothing Then cnt.MyContainer.Delete(False) + cnt.Dispose() + Next + End If + End If + If .Controls.Count > 0 Then + OffsetControls() + Else + .RowStyles.Clear() + .RowStyles.Add(New RowStyle(SizeType.AutoSize)) + .RowCount = 1 + End If + End With + UpdateScrolls(Nothing, Nothing) + End Sub, EDP.None) + End Sub + Private Overloads Sub RemoveControls(ByVal CNT As MediaItem) + ControlInvokeFast(TP_CONTROLS, Sub() + If Not CNT Is Nothing Then TP_CONTROLS.Controls.Remove(CNT) : OffsetControls() + End Sub, EDP.None) + End Sub +#End Region +#Region "Media controls' handlers" + Private Sub MediaControl_FileDownloaded(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + If MyDownloaderSettings.ShowNotifications Then MyNotificator.ShowNotification(Container.ToString(), Container.ThumbnailFile) + If MyDownloaderSettings.RemoveDownloadedAutomatically Then RemoveControls(Sender) + End Sub + Private Sub MediaControl_Removal(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + RemoveControls(Sender) + End Sub + Private Sub MediaControl_DownloadAgain(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + If Not Container.URL.IsEmptyString Then BufferText = Container.URL : BTT_ADD.PerformClick() + End Sub + Private Sub MediaControl_DownloadRequested(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + AddToDownload(Sender, True) + End Sub + Private Sub MediaControl_CheckedChanged(ByVal Sender As MediaItem, ByVal Container As IYouTubeMediaContainer) + With TP_CONTROLS.Controls + ControlInvokeFast(TOOLBAR_TOP, BTT_DELETE, + Sub() BTT_DELETE.Enabled = .Count > 0 AndAlso .Cast(Of MediaItem).ListExists(Function(cnt) cnt.Checked), EDP.None) + End With + End Sub +#End Region +#End Region +#Region "Downloading" + Protected Overridable Sub MyJob_Started(ByVal Sender As Object, ByVal e As EventArgs) Handles MyJob.Started + End Sub + Protected Overridable Sub MyJob_Finished(ByVal Sender As Object, ByVal e As EventArgs) Handles MyJob.Finished + UpdateLogButton() + End Sub + Protected Sub AddToDownload(ByRef Item As MediaItem, ByVal RunThread As Boolean) + If MyJob.Count = 0 OrElse Not MyJob.Items.Exists(Function(i) i.MyContainer.GetHashCode) Then + Item.Pending = True + MyJob.Add(Item) + Item.AddToQueue() + If RunThread Then StartDownloading() + End If + End Sub + Private Sub StartDownloading() + If Not MyJob.Working And MyJob.Count > 0 Then + EnableDownloadButtons(True) + MyJob.StartThread(AddressOf DownloadData) + End If + End Sub + Private Sub EnableDownloadButtons(ByVal Downloading As Boolean) + ControlInvoke(TOOLBAR_TOP, BTT_DOWN, Sub() + BTT_DOWN.Enabled = Not Downloading + BTT_STOP.Enabled = Downloading + End Sub, EDP.SendToLog) + End Sub + Private ReadOnly PNumProv As New ANumbers With {.FormatOptions = ANumbers.Options.GroupIntegral} + Private Sub DownloadData() + Try + MyJob.Start() + Const nf As ANumbers.Formats = ANumbers.Formats.Number + Dim t As New List(Of Task) + Dim i% + Dim __item As MediaItem + Dim Indexes As New List(Of Integer) + Dim maxJobCount% = MyDownloaderSettings.MaxJobsCount + If maxJobCount <= 0 Then maxJobCount = 1 + MyProgress.Visible = True + MyProgress.Maximum = MyJob.Count + Do While MyJob.Count > 0 And Not MyJob.IsCancellationRequested + i = -1 + Indexes.Clear() + For Each __item In MyJob.Items + i += 1 + If i <= maxJobCount - 1 Then + Indexes.Add(i) + t.Add(Task.Run(Sub() __item.Download(MyJob.Token))) + Else + Exit For + End If + Next + If t.Count > 0 Then + MyProgress.Information = $"Downloading {t.Count.NumToString(nf, PNumProv)}/{MyJob.Count.NumToString(nf, PNumProv)}" + MyProgress.InformationTemporary = MyProgress.Information + Task.WaitAll(t.ToArray) + MyProgress.Perform(t.Count) + If Indexes.Count > 0 Then + For i = Indexes.Count - 1 To 0 Step -1 : MyJob.Items.RemoveAt(Indexes(i)) : Next + End If + t.Clear() + End If + Loop + Indexes.Clear() + MyProgress.Done() + MyProgress.InformationTemporary = "Download completed" + Catch aoex As ArgumentOutOfRangeException + Catch oex As OperationCanceledException + MyProgress.InformationTemporary = "Download canceled" + Catch ex As Exception + MyProgress.InformationTemporary = "Download error" + ErrorsDescriber.Execute(EDP.SendToLog, ex, "[VideoListForm.DownloadData]") + Finally + MyJob.Finish() + EnableDownloadButtons(False) + End Try + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/MainModShared.vb b/SCrawler.YouTube/MainModShared.vb new file mode 100644 index 0000000..8c12a36 --- /dev/null +++ b/SCrawler.YouTube/MainModShared.vb @@ -0,0 +1,39 @@ +' 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.Tools +Imports SCrawler.DownloadObjects.STDownloader +Public Module MainModShared + Public Property BATCH As BatchExecutor + Private _BatchLogSent As Boolean = False + ''' + Public Sub GlobalOpenPath(ByVal f As SFile, Optional ByVal e As ErrorsDescriber = Nothing) + Dim b As Boolean = False + If Not e.Exists Then e = EDP.None + Try + If f.Exists(SFO.Path, False) Then + If MyDownloaderSettings.OpenFolderInOtherProgram AndAlso Not MyDownloaderSettings.OpenFolderInOtherProgram_Command.IsEmptyString Then + If BATCH Is Nothing Then BATCH = New BatchExecutor With {.RedirectStandardError = True} + b = True + With BATCH + .Reset() + .Execute({String.Format(MyDownloaderSettings.OpenFolderInOtherProgram_Command, f.PathWithSeparator)}, EDP.SendToLog + EDP.ThrowException) + If .HasError Or Not .ErrorOutput.IsEmptyString Then Throw New Exception(.ErrorOutput, .ErrorException) + End With + Else + f.Open(SFO.Path,, e) + End If + End If + Catch ex As Exception + If b Then + If Not _BatchLogSent Then ErrorsDescriber.Execute(EDP.SendToLog, ex, $"GlobalOpenPath({f.Path})") : _BatchLogSent = True + f.Open(SFO.Path,, e) + End If + End Try + End Sub +End Module \ No newline at end of file diff --git a/SCrawler.YouTube/My Project/Application.Designer.vb b/SCrawler.YouTube/My Project/Application.Designer.vb new file mode 100644 index 0000000..88dd01c --- /dev/null +++ b/SCrawler.YouTube/My Project/Application.Designer.vb @@ -0,0 +1,13 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + diff --git a/SCrawler.YouTube/My Project/Application.myapp b/SCrawler.YouTube/My Project/Application.myapp new file mode 100644 index 0000000..1243847 --- /dev/null +++ b/SCrawler.YouTube/My Project/Application.myapp @@ -0,0 +1,11 @@ + + + true + Form1 + false + 0 + true + 0 + 0 + true + diff --git a/SCrawler.YouTube/My Project/AssemblyInfo.vb b/SCrawler.YouTube/My Project/AssemblyInfo.vb new file mode 100644 index 0000000..cf910bd --- /dev/null +++ b/SCrawler.YouTube/My Project/AssemblyInfo.vb @@ -0,0 +1,37 @@ +Imports System.Resources +Imports System +Imports System.Reflection +Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. + +' Review the values of the assembly attributes + + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: +' + + + + diff --git a/SCrawler.YouTube/My Project/Resources.Designer.vb b/SCrawler.YouTube/My Project/Resources.Designer.vb new file mode 100644 index 0000000..c8826fb --- /dev/null +++ b/SCrawler.YouTube/My Project/Resources.Designer.vb @@ -0,0 +1,163 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + +Imports System + +Namespace My.Resources + + 'This class was auto-generated by the StronglyTypedResourceBuilder + 'class via a tool like ResGen or Visual Studio. + 'To add or remove a member, edit your .ResX file then rerun ResGen + 'with the /str option, or rebuild your VS project. + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + _ + Public Module Resources + + Private resourceMan As Global.System.Resources.ResourceManager + + Private resourceCulture As Global.System.Globalization.CultureInfo + + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + _ + Public ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager + Get + If Object.ReferenceEquals(resourceMan, Nothing) Then + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("SCrawler.Resources", GetType(Resources).Assembly) + resourceMan = temp + End If + Return resourceMan + End Get + End Property + + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + _ + Public Property Culture() As Global.System.Globalization.CultureInfo + Get + Return resourceCulture + End Get + Set + resourceCulture = value + End Set + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property ArrowDownPic_Blue_24() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("ArrowDownPic_Blue_24", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property AudioMusic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("AudioMusic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property ClockPic_16() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("ClockPic_16", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property HeartPic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("HeartPic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property ImagePic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("ImagePic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property InfoPic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("InfoPic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property LinkPic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("LinkPic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property RulerPic_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("RulerPic_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property SettingsPic_16() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("SettingsPic_16", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public ReadOnly Property VideoCamera_32() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("VideoCamera_32", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + End Module +End Namespace diff --git a/SCrawler.YouTube/My Project/Resources.resx b/SCrawler.YouTube/My Project/Resources.resx new file mode 100644 index 0000000..7d3d570 --- /dev/null +++ b/SCrawler.YouTube/My Project/Resources.resx @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Content\Pictures\ArrowDownPic_Blue_24.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\AudioMusic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\ClockPic_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\HeartPic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\ImagePic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\InfoPic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\LinkPic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\RulerPic_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\SettingsPic_16.bmp;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Pictures\VideoCamera_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/SCrawler.YouTube/My Project/Settings.Designer.vb b/SCrawler.YouTube/My Project/Settings.Designer.vb new file mode 100644 index 0000000..fcfd812 --- /dev/null +++ b/SCrawler.YouTube/My Project/Settings.Designer.vb @@ -0,0 +1,73 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My + + _ + Partial Friend NotInheritable Class MySettings + Inherits Global.System.Configuration.ApplicationSettingsBase + + Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings()),MySettings) + +#Region "My.Settings Auto-Save Functionality" +#If _MyType = "WindowsForms" Then + Private Shared addedHandler As Boolean + + Private Shared addedHandlerLockObject As New Object + + _ + Private Shared Sub AutoSaveSettings(sender As Global.System.Object, e As Global.System.EventArgs) + If My.Application.SaveMySettingsOnExit Then + My.Settings.Save() + End If + End Sub +#End If +#End Region + + Public Shared ReadOnly Property [Default]() As MySettings + Get + +#If _MyType = "WindowsForms" Then + If Not addedHandler Then + SyncLock addedHandlerLockObject + If Not addedHandler Then + AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings + addedHandler = True + End If + End SyncLock + End If +#End If + Return defaultInstance + End Get + End Property + End Class +End Namespace + +Namespace My + + _ + Friend Module MySettingsProperty + + _ + Friend ReadOnly Property Settings() As Global.SCrawler.My.MySettings + Get + Return Global.SCrawler.My.MySettings.Default + End Get + End Property + End Module +End Namespace diff --git a/SCrawler.YouTube/My Project/Settings.settings b/SCrawler.YouTube/My Project/Settings.settings new file mode 100644 index 0000000..85b890b --- /dev/null +++ b/SCrawler.YouTube/My Project/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SCrawler.YouTube/Objects/Channel.vb b/SCrawler.YouTube/Objects/Channel.vb new file mode 100644 index 0000000..95298d2 --- /dev/null +++ b/SCrawler.YouTube/Objects/Channel.vb @@ -0,0 +1,98 @@ +' 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.XML +Imports PersonalUtilities.Forms.Toolbars +Namespace API.YouTube.Objects + Public Class Channel : Inherits YouTubeMediaContainerBase + Public Sub New() + ObjectType = Base.YouTubeMediaType.Channel + End Sub + Public Overrides Function ToString(ByVal ForMediaItem As Boolean) As String + Return UserTitle + End Function + Public Overrides Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean + _MediaType = IIf(IsMusic, Plugin.UserMediaTypes.Audio, Plugin.UserMediaTypes.Video) + _ObjectType = Base.YouTubeMediaType.Channel + Me.IsMusic = IsMusic + If ParseFiles(Path, IsMusic, Token, Progress) Then + PlaylistID = String.Empty + PlaylistIndex = 0 + PlaylistTitle = String.Empty + ThrowAny(Token) + + 'Reconfiguration + If IsMusic AndAlso HasElements AndAlso Elements.Exists(Function(e) Not e.PlaylistID.IsEmptyString) Then + Dim elems As New List(Of IYouTubeMediaContainer)(Elements) + Dim elemsNew As New List(Of IYouTubeMediaContainer) + Dim playlistDic As New Dictionary(Of String, List(Of IYouTubeMediaContainer)) + Elements.Clear() + For Each elem In elems + If Not elem.PlaylistTitle.IsEmptyString Then + If Not playlistDic.ContainsKey(elem.PlaylistTitle) Then playlistDic.Add(elem.PlaylistTitle, New List(Of IYouTubeMediaContainer)) + playlistDic(elem.PlaylistTitle).Add(elem) + ElseIf elem.PlaylistID = elem.UserID Then + elem.PlaylistID = String.Empty + elem.PlaylistIndex = -1 + elem.PlaylistTitle = String.Empty + elemsNew.Add(elem) + Else + elemsNew.Add(elem) + End If + Next + If playlistDic.Count > 0 Then + Dim i%, ii% + Dim v As YouTubeMediaContainerBase + For Each kv In playlistDic + i = -1 + If elemsNew.Count > 0 Then i = elemsNew.FindIndex(Function(e) e.PlaylistID = kv.Key) + If i = -1 Then + elemsNew.Add(New PlayList) + v = kv.Value.First + With DirectCast(elemsNew.Last, YouTubeMediaContainerBase) + .ObjectType = Base.YouTubeMediaType.PlayList + .MediaType = v.MediaType + .IsMusic = v.IsMusic + .ID = v.PlaylistID + .Title = v.PlaylistTitle + .PlaylistID = .ID + .PlaylistTitle = .Title + .PlaylistIndex = -1 + .UserID = v.UserID + .UserTitle = v.UserTitle + End With + i = elemsNew.Count - 1 + End If + With elemsNew(i).Elements + .AddRange(kv.Value) + If .Count > 0 Then + For ii = 0 To .Count - 1 + With DirectCast(.Item(ii), YouTubeMediaContainerBase) + .PlaylistIndex = ii + 1 + .PlaylistCount = kv.Value.Count + End With + Next + End If + End With + Next + playlistDic.Clear() + End If + If elemsNew.Count > 0 Then Elements.AddRange(elemsNew) + elems.Clear() + elemsNew.Clear() + File = MyYouTubeSettings.OutputPath + End If + + Return True + Else + Return False + End If + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Objects/IYouTubeMediaContainer.vb b/SCrawler.YouTube/Objects/IYouTubeMediaContainer.vb new file mode 100644 index 0000000..2d2c7da --- /dev/null +++ b/SCrawler.YouTube/Objects/IYouTubeMediaContainer.vb @@ -0,0 +1,88 @@ +' 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.Plugin +Imports SCrawler.API.YouTube.Base +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Forms.Toolbars +Imports UMTypes = SCrawler.Plugin.UserMediaTypes +Imports UMStates = SCrawler.Plugin.UserMediaStates +Namespace API.YouTube.Objects + Public Interface IYouTubeMediaContainer : Inherits IDownloadableMedia, IEContainerProvider, IComparable(Of IYouTubeMediaContainer) +#Region "Events" + Event FileDownloaded As EventHandler + Event FileDownloadStarted As EventHandler + Event DataDownloaded As EventHandler +#End Region +#Region "Base data" + ReadOnly Property ObjectType As YouTubeMediaType + ReadOnly Property MediaType As UMTypes + ReadOnly Property MediaState As UMStates + Property IsMusic As Boolean + Property ID As String + Property Description As String + Property PlaylistID As String + Property PlaylistTitle As String + Property UserID As String + Property UserTitle As String +#End Region +#Region "Playlist support" + ReadOnly Property Elements As List(Of IYouTubeMediaContainer) + ReadOnly Property HasElements As Boolean + ReadOnly Property Count As Integer + Property PlaylistIndex As Integer +#End Region +#Region "Data info" +#Region "Thumbnails" + ReadOnly Property Thumbnails As List(Of Thumbnail) + ReadOnly Property ThumbnailUrlMedia As String + Overloads ReadOnly Property ThumbnailFile As SFile +#End Region +#Region "Subtitles" + ReadOnly Property Subtitles As List(Of Subtitles) + ReadOnly Property SubtitlesSelectedIndexes As List(Of Integer) +#End Region +#Region "MediaObjects" + ReadOnly Property MediaObjects As List(Of MediaObject) + Property SelectedAudioIndex As Integer + Property SelectedVideoIndex As Integer +#End Region + ReadOnly Property SizeStr As String + Property Height As Integer + Property Bitrate As Integer + Property DateCreated As Date + Property DateAdded As Date + Property DateDownloaded As Date + Property OutputVideoExtension As String + Property OutputAudioCodec As String + Property OutputSubtitlesFormat As String +#End Region +#Region "HasError, Exists, Checked" + ReadOnly Property CheckState As CheckState +#End Region +#Region "URL, File, Command" + Overloads Property File As SFile + ReadOnly Property Files As List(Of SFile) + Property UseCookies As Boolean + ReadOnly Property Command(ByVal WithCookies As Boolean) As String + Sub UpdateInfoFields() +#End Region +#Region "Download" + Overloads Property Progress As MyProgress +#End Region +#Region "Parse, Load" + Overloads Sub Load(ByVal f As SFile) + Overloads Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean +#End Region +#Region "IDisposable" + ReadOnly Property IsDisposed As Boolean +#End Region + End Interface +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Objects/PlayList.vb b/SCrawler.YouTube/Objects/PlayList.vb new file mode 100644 index 0000000..e418238 --- /dev/null +++ b/SCrawler.YouTube/Objects/PlayList.vb @@ -0,0 +1,43 @@ +' 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.XML +Imports PersonalUtilities.Forms.Toolbars +Namespace API.YouTube.Objects + Public Class PlayList : Inherits YouTubeMediaContainerBase + Public Sub New() + _ObjectType = Base.YouTubeMediaType.PlayList + End Sub + Public Overrides Function ToString(ByVal ForMediaItem As Boolean) As String + Dim t$ = String.Empty + Dim s$ = SizeStr + Dim __title$ = $" - {Title}" + If Not s.IsEmptyString Then s = $" [{s}]" + If Not PlaylistTitle.IsEmptyString And Not ForMediaItem Then t = $"{PlaylistTitle} - " + If IsMusic Then + If Count <= 1 Then t &= "Single" Else t &= "Album" + Else + t &= "Playlist" + End If + If Not PlaylistTitle.IsEmptyString And Not ForMediaItem Then t &= $" - {PlaylistTitle}" + If PlaylistTitle = Title Then __title = String.Empty + If ForMediaItem Then + Return $"{t} ({Count}){__title}" + Else + Return $"{t} ({Count}){__title} ({AConvert(Of String)(Duration, TimeToStringProvider)}){s}" + End If + End Function + Public Overrides Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean + _MediaType = IIf(IsMusic, Plugin.UserMediaTypes.Audio, Plugin.UserMediaTypes.Video) + _ObjectType = Base.YouTubeMediaType.PlayList + Me.IsMusic = IsMusic + Return ParseFiles(Path, IsMusic, Token, Progress) + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Objects/Track.vb b/SCrawler.YouTube/Objects/Track.vb new file mode 100644 index 0000000..1cee2bb --- /dev/null +++ b/SCrawler.YouTube/Objects/Track.vb @@ -0,0 +1,61 @@ +' 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.XML +Imports PersonalUtilities.Forms.Toolbars +Namespace API.YouTube.Objects + Public Class Track : Inherits YouTubeMediaContainerBase + Public Sub New() + IsMusic = True + ObjectType = Base.YouTubeMediaType.Single + End Sub + Protected Overrides Sub GenerateFileName() + If Not FileSetManually Or _File.IsEmptyString Then + Dim indx$ = String.Empty + If PlaylistIndex > 0 Then indx = PlaylistIndex.NumToString(ANumbers.Formats.NumberGroup, PlaylistCount.ToString.Length) + If Not indx.IsEmptyString Then indx &= ". " + _File.Name = $"{indx}{Title}" + If Not OutputAudioCodec.IsEmptyString Then + _File.Extension = OutputAudioCodec.StringToLower + ElseIf Not MyYouTubeSettings.DefaultAudioCodecMusic.IsEmptyString Then + _File.Extension = MyYouTubeSettings.DefaultAudioCodecMusic.Value.StringToLower + Else + _File.Extension = mp3 + End If + End If + End Sub + Public Overrides Function ToString(ByVal ForMediaItem As Boolean) As String + Dim s$ = SizeStr + If Not s.IsEmptyString Then s = $" [{s}]" + Dim pls$ = String.Empty + If PlaylistIndex > 0 Then pls = $"{PlaylistIndex.NumToString(ANumbers.Formats.NumberGroup, PlaylistCount.ToString.Length)}. " + If ForMediaItem Then + Return Title + Else + Return $"{pls}{Title} ({AConvert(Of String)(Duration, TimeToStringProvider)}){s}" + End If + End Function + Public Overrides Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean + _MediaType = Plugin.UserMediaTypes.Audio + _ObjectType = Base.YouTubeMediaType.Single + Me.IsMusic = IsMusic + If MyBase.Parse(Container, Path, IsMusic, Token, Progress) Then + Dim f As SFile = MyYouTubeSettings.OutputPath + If f.IsEmptyString Then f = "YouTubeDownloads\OutputFile.mp3" + Dim ext$ = MyYouTubeSettings.DefaultAudioCodec.Value.StringToLower + If ext.IsEmptyString Then ext = "mp3" + f.Extension = ext + File = f + Return True + Else + Return False + End If + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Objects/Video.vb b/SCrawler.YouTube/Objects/Video.vb new file mode 100644 index 0000000..d99ef8c --- /dev/null +++ b/SCrawler.YouTube/Objects/Video.vb @@ -0,0 +1,29 @@ +' 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.YouTube.Base +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Forms.Toolbars +Namespace API.YouTube.Objects + Public Class Video : Inherits YouTubeMediaContainerBase + Public Sub New() + _ObjectType = YouTubeMediaType.Single + _MediaType = Plugin.UserMediaTypes.Video + End Sub + Public Overrides Function ToString(ByVal ForMediaItem As Boolean) As String + Return Title + End Function + Public Overrides Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As Threading.CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean + _MediaType = Plugin.UserMediaTypes.Video + _ObjectType = YouTubeMediaType.Single + Me.IsMusic = IsMusic + Return MyBase.Parse(Container, Path, IsMusic, Token, Progress) + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/Objects/YouTubeMediaContainerBase.vb b/SCrawler.YouTube/Objects/YouTubeMediaContainerBase.vb new file mode 100644 index 0000000..8460109 --- /dev/null +++ b/SCrawler.YouTube/Objects/YouTubeMediaContainerBase.vb @@ -0,0 +1,1457 @@ +' 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.Plugin +Imports SCrawler.API.YouTube.Base +Imports PersonalUtilities.Forms.Toolbars +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.XML.Base +Imports PersonalUtilities.Functions.XML.Attributes +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON +Imports UMTypes = SCrawler.Plugin.UserMediaTypes +Imports UMStates = SCrawler.Plugin.UserMediaStates +Imports CollectionModes = PersonalUtilities.Functions.XML.Objects.IXMLValuesCollection.Modes +Namespace API.YouTube.Objects + Public Class ContainerDateComparer : Implements IComparer(Of IYouTubeMediaContainer) + Private ReadOnly NullDateValue As New Date + Public Function Compare(ByVal x As IYouTubeMediaContainer, ByVal y As IYouTubeMediaContainer) As Integer Implements IComparer(Of IYouTubeMediaContainer).Compare + If x.DateDownloaded = NullDateValue And y.DateDownloaded = NullDateValue Then + Return x.DateCreated.CompareTo(y.DateCreated) * -1 + ElseIf x.DateDownloaded = NullDateValue Then + Return -1 + ElseIf y.DateDownloaded = NullDateValue Then + Return 1 + Else + Return x.DateDownloaded.CompareTo(y.DateDownloaded) * -1 + End If + End Function + End Class + Public MustInherit Class YouTubeMediaContainerBase : Implements IYouTubeMediaContainer +#Region "Events" + Public Event CheckedChange As EventHandler Implements IDownloadableMedia.CheckedChange + Public Event FileDownloaded As EventHandler Implements IYouTubeMediaContainer.FileDownloaded + Public Event FileDownloadStarted As EventHandler Implements IYouTubeMediaContainer.FileDownloadStarted + Public Event DataDownloaded As EventHandler Implements IYouTubeMediaContainer.DataDownloaded + Public Event ThumbnailChanged As EventHandler Implements IDownloadableMedia.ThumbnailChanged + Public Event StateChanged As EventHandler Implements IDownloadableMedia.StateChanged +#End Region +#Region "XML names" + Protected Friend Const Name_ObjectType As String = "ObjectType" + Protected Friend Const Name_MediaType As String = "MediaType" + Protected Friend Const Name_SiteKey As String = "SiteKey" + Protected Friend Const Name_IsMusic As String = "IsMusic" + Protected Friend Const Name_CachePath As String = "CachePath" + + Private Const Name_CheckedElements As String = "CheckedElements" + Private Const Name_CheckedAttribute As String = "Checked" +#End Region +#Region "Base data" + Protected _ObjectType As YouTubeMediaType = YouTubeMediaType.Undefined + Public Property ObjectType As YouTubeMediaType Implements IYouTubeMediaContainer.ObjectType + Get + Return _ObjectType + End Get + Set(ByVal t As YouTubeMediaType) + _ObjectType = t + End Set + End Property + Protected _MediaType As UMTypes = UMTypes.Undefined + Public Property MediaType As UMTypes Implements IYouTubeMediaContainer.MediaType, IUserMedia.ContentType + Get + Return _MediaType + End Get + Set(ByVal t As UMTypes) + _MediaType = t + End Set + End Property + Protected _MediaState As UMStates = UMStates.Unknown + Public Property MediaState As UMStates Implements IYouTubeMediaContainer.MediaState, IUserMedia.DownloadState + Get + If _MediaState = UMStates.Unknown And HasElements Then + Return If(Elements.Exists(Function(e) e.MediaState = UMStates.Downloaded), UMStates.Downloaded, _MediaState) + Else + Return _MediaState + End If + End Get + Set(ByVal s As UMStates) + _MediaState = s + End Set + End Property + Protected _SiteIcon As Image = Nothing + Protected _SiteIconSetManually As Boolean = False + Public Property SiteIcon As Image Implements IDownloadableMedia.SiteIcon + Get + If _SiteIconSetManually Then + Return _SiteIcon + Else + Return If(IsMusic, My.Resources.SiteYouTube.YouTubeMusicPic_96, My.Resources.SiteYouTube.YouTubePic_96) + End If + End Get + Set(ByVal Img As Image) + _SiteIcon = Img + _SiteIconSetManually = True + End Set + End Property + Protected _Site As String = YouTubeSite + Public Property Site As String Implements IDownloadableMedia.Site + Get + Return _Site + End Get + Set(ByVal s As String) + _Site = s + End Set + End Property + Protected _SiteKey As String = YouTubeSiteKey + Public Property SiteKey As String Implements IDownloadableMedia.SiteKey + Get + Return _SiteKey + End Get + Set(ByVal Key As String) + _SiteKey = Key + End Set + End Property + Public Property IsMusic As Boolean = False Implements IYouTubeMediaContainer.IsMusic + Public Property IsShorts As Boolean = False + Public Property ID As String Implements IYouTubeMediaContainer.ID, IUserMedia.PostID + Public Property Title As String Implements IDownloadableMedia.Title + Public Property Description As String Implements IYouTubeMediaContainer.Description + Public Property PlaylistID As String Implements IYouTubeMediaContainer.PlaylistID + Public Property PlaylistTitle As String Implements IYouTubeMediaContainer.PlaylistTitle + Public Property UserID As String Implements IYouTubeMediaContainer.UserID + Public Property UserTitle As String Implements IYouTubeMediaContainer.UserTitle +#End Region +#Region "Playlist support" + Friend ReadOnly Property Elements As List(Of IYouTubeMediaContainer) Implements IYouTubeMediaContainer.Elements + Friend ReadOnly Property HasElements As Boolean Implements IYouTubeMediaContainer.HasElements + Get + Return Count > 0 + End Get + End Property + Friend ReadOnly Property Count As Integer Implements IYouTubeMediaContainer.Count + Get + Return Elements.Count + End Get + End Property + Public Property PlaylistIndex As Integer = -1 Implements IYouTubeMediaContainer.PlaylistIndex + Protected Friend PlaylistCount As Integer = 0 +#End Region +#Region "Data info" + Friend ReadOnly Property MediaObjects As List(Of MediaObject) Implements IYouTubeMediaContainer.MediaObjects +#Region "Array" + ''' [-10] = disabled; [-1] = max; [-2] = audio only + Friend Property ArrayMaxResolution As Integer = -10 + ''' [-1] = max; [-2] = audio only + Friend Sub SetMaxResolution(ByVal Value As Integer) + ArrayMaxResolution = Value + SelectedVideoIndex = -1 + If MediaObjects.Count > 0 And Value <> -2 Then + If Value = -1 Then + SelectedVideoIndex = MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Video) + Else + SelectedVideoIndex = MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Video And mo.Height <= Value) + If SelectedVideoIndex = -1 Then SelectedVideoIndex = MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Video) + End If + End If + If HasElements Then Elements.ForEach(Sub(e As YouTubeMediaContainerBase) e.SetMaxResolution(Value)) + End Sub +#End Region +#Region "Thumbnails" + Public ReadOnly Property Thumbnails As List(Of Thumbnail) Implements IYouTubeMediaContainer.Thumbnails + Protected _ThumbnailUrl As String = String.Empty + Public Overridable Property ThumbnailUrl As String Implements IDownloadableMedia.ThumbnailUrl + Get + If _ThumbnailUrl.IsEmptyString And Thumbnails.Count > 0 Then + Return Thumbnails.FirstOrDefault.URL + Else + Return _ThumbnailUrl + End If + End Get + Set(ByVal url As String) + _ThumbnailUrl = url + End Set + End Property + Public ReadOnly Property ThumbnailUrlMedia As String Implements IYouTubeMediaContainer.ThumbnailUrlMedia + Get + If _ThumbnailUrl.IsEmptyString And Thumbnails.Count > 0 Then + Dim u$ = Thumbnails.FirstOrDefault(Function(t) Not t.URL.Contains(".webp")).URL + If u.IsEmptyString Then u = Thumbnails.First.URL + If u.IsEmptyString Then Return ThumbnailUrl Else Return u + ElseIf HasElements Then + Return If(Elements.FirstOrDefault(Function(e) Not e.ThumbnailUrlMedia.IsEmptyString)?.ThumbnailUrlMedia, String.Empty).IfNullOrEmpty(_ThumbnailUrl) + Else + Return _ThumbnailUrl + End If + End Get + End Property + Protected _ThumbnailFile As SFile = Nothing + Public ReadOnly Property ThumbnailFile As SFile Implements IYouTubeMediaContainer.ThumbnailFile + Get + Return _ThumbnailFile + End Get + End Property + Private Property IDownloadableMedia_ThumbnailFile As String Implements IDownloadableMedia.ThumbnailFile + Get + Return ThumbnailFile + End Get + Set(ByVal f As String) + _ThumbnailFile = f + End Set + End Property +#End Region +#Region "Video" + Friend Property SelectedVideoIndex As Integer = -1 Implements IYouTubeMediaContainer.SelectedVideoIndex + Friend ReadOnly Property SelectedVideo As MediaObject + Get + If SelectedVideoIndex >= 0 Then Return MediaObjects(SelectedVideoIndex) Else Return Nothing + End Get + End Property + Protected _OutputVideoExtension As String + Friend Property OutputVideoExtension As String Implements IYouTubeMediaContainer.OutputVideoExtension + Get + Return _OutputVideoExtension + End Get + Set(ByVal _OutputVideoExtension As String) + Me._OutputVideoExtension = _OutputVideoExtension + If HasElements Then Elements.ForEach(Sub(e) e.OutputVideoExtension = _OutputVideoExtension) + End Set + End Property +#End Region +#Region "Audio" + Friend Property SelectedAudioIndex As Integer = -1 Implements IYouTubeMediaContainer.SelectedAudioIndex + Friend ReadOnly Property SelectedAudio As MediaObject + Get + If SelectedAudioIndex >= 0 Then Return MediaObjects(SelectedAudioIndex) Else Return Nothing + End Get + End Property + Protected _OutputAudioCodec As String + Friend Property OutputAudioCodec As String Implements IYouTubeMediaContainer.OutputAudioCodec + Get + Return _OutputAudioCodec + End Get + Set(ByVal _OutputAudioCodec As String) + Me._OutputAudioCodec = _OutputAudioCodec + If HasElements Then Elements.ForEach(Sub(e) e.OutputAudioCodec = _OutputAudioCodec) + End Set + End Property + + Friend ReadOnly Property PostProcessing_OutputAudioFormats As List(Of String) + Friend Sub PostProcessing_OutputAudioFormats_Reset() + PostProcessing_OutputAudioFormats.Clear() + PostProcessing_OutputAudioFormats.ListAddList(MyYouTubeSettings.DefaultAudioCodecAddit) + If PostProcessing_OutputAudioFormats.Count > 0 Then + PostProcessing_OutputAudioFormats.Sort() + PostProcessing_OutputAudioFormats.RemoveAll(Function(s) s = -1) + End If + End Sub +#End Region +#Region "Subtitles" + Protected ReadOnly _Subtitles As List(Of Subtitles) + Private ReadOnly _SubtitlesDelegated As List(Of Subtitles) + Friend ReadOnly Property Subtitles As List(Of Subtitles) Implements IYouTubeMediaContainer.Subtitles + Get + If HasElements Then + If _SubtitlesDelegated.Count > 0 Then + Return _SubtitlesDelegated + Else + Return _Subtitles.Concat(Elements.SelectMany(Function(e) e.Subtitles)).Distinct.ListIfNothing.ListSort + End If + ElseIf _SubtitlesDelegated.Count > 0 Then + Return _SubtitlesDelegated + Else + Return _Subtitles + End If + End Get + End Property + + Friend ReadOnly Property SubtitlesSelectedIndexes As List(Of Integer) Implements IYouTubeMediaContainer.SubtitlesSelectedIndexes + Protected _OutputSubtitlesFormat As String + Friend Property OutputSubtitlesFormat As String Implements IYouTubeMediaContainer.OutputSubtitlesFormat + Get + Return _OutputSubtitlesFormat + End Get + Set(ByVal _OutputSubtitlesFormat As String) + Me._OutputSubtitlesFormat = _OutputSubtitlesFormat + If HasElements Then Elements.ForEach(Sub(e) e.OutputSubtitlesFormat = _OutputSubtitlesFormat) + End Set + End Property + + Friend ReadOnly Property PostProcessing_OutputSubtitlesFormats As List(Of String) + Friend Sub PostProcessing_OutputSubtitlesFormats_Reset() + PostProcessing_OutputSubtitlesFormats.Clear() + PostProcessing_OutputSubtitlesFormats.ListAddList(MyYouTubeSettings.DefaultSubtitlesFormatAddit) + If PostProcessing_OutputSubtitlesFormats.Count > 0 Then + PostProcessing_OutputSubtitlesFormats.Sort() + PostProcessing_OutputSubtitlesFormats.RemoveAll(Function(s) s = -1) + End If + End Sub + Friend Sub SubtitlesSelectedIndexesReset() + SubtitlesSelectedIndexes.Clear() + Dim subs As List(Of Subtitles) = Subtitles + SubtitlesSelectedIndexes.ListAddList(MyYouTubeSettings.DefaultSubtitles.Select(Function(s) subs.FindIndex(Function(ss) ss.ID = s))) + If SubtitlesSelectedIndexes.Count > 0 Then + SubtitlesSelectedIndexes.Sort() + SubtitlesSelectedIndexes.RemoveAll(Function(s) s = -1) + End If + End Sub + Private Sub SetElementsSubtitles(ByVal Source As YouTubeMediaContainerBase) + If Not Source Is Nothing And HasElements Then + Dim subs As List(Of Subtitles) = Source.Subtitles + For Each elem As YouTubeMediaContainerBase In Elements + With elem + ._SubtitlesDelegated.Clear() + If subs.Count > 0 Then ._SubtitlesDelegated.AddRange(subs) + .SubtitlesSelectedIndexes.Clear() + If Source.SubtitlesSelectedIndexes.Count > 0 Then .SubtitlesSelectedIndexes.AddRange(Source.SubtitlesSelectedIndexes) + .OutputSubtitlesFormat = Source.OutputSubtitlesFormat + .PostProcessing_OutputSubtitlesFormats.Clear() + If Source.PostProcessing_OutputSubtitlesFormats.Count > 0 Then .PostProcessing_OutputSubtitlesFormats.AddRange(Source.PostProcessing_OutputSubtitlesFormats) + End With + Next + End If + End Sub +#End Region +#Region "IUserMedia Support" + Private Property Attempts As Integer Implements IUserMedia.Attempts + Private _Object As Object = Nothing + Private Property [Object] As Object Implements IUserMedia.Object + Get + Return If(_Object, Me) + End Get + Set(ByVal Obj As Object) + _Object = Obj + End Set + End Property + Private Property IUserMedia_MD5 As String Implements IUserMedia.MD5 + Public Shared Sub Update(ByVal Source As IUserMedia, ByVal Destination As IYouTubeMediaContainer) + If Not Source Is Nothing And Not Destination Is Nothing Then + Destination.ContentType = Source.ContentType + Destination.URL = Source.URL + Destination.URL_BASE = Source.URL_BASE + Destination.MD5 = Source.MD5 + Destination.File = Source.File + Destination.DownloadState = Source.DownloadState + Destination.ID = Source.PostID + Destination.PostDate = Source.PostDate + Destination.SpecialFolder = Source.SpecialFolder + Destination.Attempts = Source.Attempts + End If + End Sub +#End Region + Protected _Duration As TimeSpan = Nothing + + Public Overridable Property Duration As TimeSpan Implements IDownloadableMedia.Duration + Get + If HasElements Then + Return TimeSpan.FromSeconds(Elements.Sum(Function(e) If(e.Checked, e.Duration.TotalSeconds, 0))) + Else + Return _Duration + End If + End Get + Set(ByVal d As TimeSpan) + _Duration = d + End Set + End Property + Protected _Size As Integer = 0 + Public Overridable Property Size As Integer Implements IDownloadableMedia.Size + Get + If HasElements Then + Return Elements.Sum(Function(e) If(e.Checked, e.Size, 0)) + Else + If Checked Then + If IsMusic And SelectedAudioIndex.ValueBetween(0, MediaObjects.Count - 1) Then + Return MediaObjects(SelectedAudioIndex).Size + ElseIf Not IsMusic And SelectedVideoIndex.ValueBetween(0, MediaObjects.Count - 1) Then + Return MediaObjects(SelectedVideoIndex).Size + + If(SelectedAudioIndex.ValueBetween(0, MediaObjects.Count - 1), MediaObjects(SelectedAudioIndex).Size, 0) + Else + Return _Size + End If + Else + Return 0 + End If + End If + End Get + Set(ByVal s As Integer) + _Size = s + End Set + End Property + Public ReadOnly Property SizeStr As String Implements IYouTubeMediaContainer.SizeStr + Get + If Size > 0 Then + Dim sv% = Size / 1024 + Dim value$ + If sv >= 1000 Then + value = AConvert(Of String)(sv / 1024, VideoSizeProvider) + value &= " GB" + Else + value = AConvert(Of String)(sv, VideoSizeProvider) + value &= " MB" + End If + Return value + Else + Return String.Empty + End If + End Get + End Property + Public Property Height As Integer Implements IYouTubeMediaContainer.Height + Protected _Bitrate As Integer = 0 + Public Overridable Property Bitrate As Integer Implements IYouTubeMediaContainer.Bitrate + Get + If HasElements Then + Try + Return Elements.Average(Function(e) e.Bitrate) + Catch + Return _Bitrate + End Try + Else + Return _Bitrate + End If + End Get + Set(ByVal _Bitrate As Integer) + Me._Bitrate = _Bitrate + End Set + End Property + Public Property DateCreated As Date = Now Implements IYouTubeMediaContainer.DateCreated + Public Property DateAdded As Date Implements IYouTubeMediaContainer.DateAdded + Private Property IUserMedia_PostDate As Date? Implements IUserMedia.PostDate + Get + Return DateAdded + End Get + Set(ByVal d As Date?) + If d.HasValue Then DateAdded = d.Value Else DateAdded = New Date + End Set + End Property + Public Property DateDownloaded As Date Implements IYouTubeMediaContainer.DateDownloaded +#End Region +#Region "HasError, Exists" + Protected _HasError As Boolean = False + Public ReadOnly Property HasError As Boolean Implements IDownloadableMedia.HasError + Get + Return _HasError + End Get + End Property + Protected _Exists As Boolean = True + Public ReadOnly Property Exists As Boolean Implements IDownloadableMedia.Exists + Get + If Not _Exists Then + Return False + ElseIf Me.MediaState = UMStates.Downloaded Then + Return _Exists + ElseIf ObjectType = YouTubeMediaType.PlayList Or ObjectType = YouTubeMediaType.Channel Then + Return HasElements + Else + Return MediaObjects.Count > 0 + End If + End Get + End Property + Protected Overridable Property IDownloadableMedia_Instance As IPluginContentProvider Implements IDownloadableMedia.Instance +#End Region +#Region "Checked" + Protected _Checked As Boolean = True + Public Property Checked As Boolean Implements IDownloadableMedia.Checked + Get + If HasElements Then + Return Elements.Exists(Function(e) e.Checked) + Else + Return _Checked + End If + End Get + Set(ByVal _Checked As Boolean) + Dim b As Boolean = Not Me._Checked = _Checked + Me._Checked = _Checked + If HasElements Then Elements.ForEach(Sub(e) e.Checked = _Checked) + If b Then RaiseEvent CheckedChange(Me, Nothing) + End Set + End Property + Public ReadOnly Property CheckState As CheckState Implements IYouTubeMediaContainer.CheckState + Get + If HasElements Then + Dim ecs As IEnumerable(Of CheckState) = Elements.Select(Function(e) e.CheckState) + If ecs.All(Function(c) c = CheckState.Checked) Then + Return CheckState.Checked + ElseIf ecs.All(Function(c) c = CheckState.Unchecked) Then + Return CheckState.Unchecked + Else + Return CheckState.Indeterminate + End If + ElseIf Checked Then + Return CheckState.Checked + Else + Return CheckState.Unchecked + End If + End Get + End Property +#End Region +#Region "URL, File, Files, CachePath, SpecialPath, FileSettings" + Private CachePath As SFile + Public Property URL As String Implements IUserMedia.URL + Private _IUserMedia_URL_BASE As String = String.Empty + Private Property IUserMedia_URL_BASE As String Implements IUserMedia.URL_BASE + Get + Return _IUserMedia_URL_BASE.IfNullOrEmpty(URL) + End Get + Set(ByVal u As String) + _IUserMedia_URL_BASE = u + End Set + End Property + Protected Overridable Sub GenerateFileName() + End Sub + Protected Function GetPlayListTitle() As String + Dim plsTitle$ = String.Empty + If IsMusic And Not DateAdded = New Date Then plsTitle = $"{DateAdded.Year} - " + plsTitle &= PlaylistTitle + If IsShorts Then plsTitle &= " - Shorts" + Return plsTitle + End Function + Public Property SpecialPathDisabled As Boolean = False + Protected _SpecialPath As String = String.Empty + Public Property SpecialPath As String Implements IUserMedia.SpecialFolder + Get + If SpecialPathDisabled Then + Return String.Empty + ElseIf Not _SpecialPath.IsEmptyString Then + Return _SpecialPath + ElseIf IsShorts Then + Return "Shorts" + ElseIf HasElements Or PlaylistCount > 0 Then + Return PlaylistTitle.IfNullOrEmpty(Title).IfNullOrEmpty(UserTitle) + Else + Return String.Empty + End If + End Get + Set(ByVal p As String) + _SpecialPath = p + End Set + End Property + Public Sub SpecialPathSetForPlaylist(ByVal Path As String) + _SpecialPath = Path + _FileIsPlaylistObject = True + If ObjectType = YouTubeMediaType.Single AndAlso Not GetPlayListTitle.IsEmptyString Then _SpecialPath.StringAppend(GetPlayListTitle(), "\") + If Elements.Count > 0 Then Elements.ForEach(Sub(e) e.SpecialFolder = Path) + End Sub + Friend ReadOnly Property Files As List(Of SFile) Implements IYouTubeMediaContainer.Files + Protected _File As SFile + Protected Friend Property FileSetManually As Boolean = False + Public Property FileIgnorePlaylist As Boolean = False + Private _FileIsPlaylistObject As Boolean = False + ''' Compatible property for IUserMedia. Default: . + ''' DON'T USE IN STD! + Public ReadOnly Property FileIsPlaylistObject As Boolean + Get + Return _FileIsPlaylistObject + End Get + End Property + Public Overridable Property File As SFile Implements IYouTubeMediaContainer.File + Get + Return _File + End Get + Set(ByVal f As SFile) + Select Case ObjectType + Case YouTubeMediaType.Channel : _File = f.Path + Case YouTubeMediaType.PlayList : _File.Path = $"{f.PathWithSeparator}{GetPlayListTitle()}" + Case YouTubeMediaType.Single + If PlaylistCount > 0 And Not FileIgnorePlaylist Then + _File.Path = f.Path + Dim pls$ = GetPlayListTitle() + If Not _File.Path.Contains(pls) Then _File.Path = $"{_File.PathWithSeparator(Not pls.IsEmptyString)}{pls}" + ElseIf Not f.Name.IsEmptyString Then + _File = f + Else + _File.Path = f.Path + End If + Case Else : _File = f + End Select + GenerateFileName() + If HasElements Then Elements.ForEach(Sub(e) e.File = _File) + End Set + End Property + Public Property FileSettings As SFile + Private Property IUserMedia_File As String Implements IUserMedia.File + Get + Return File + End Get + Set(ByVal f As String) + File = f + End Set + End Property +#End Region +#Region "Command" + Public Property UseCookies As Boolean = MyYouTubeSettings.DefaultUseCookies Implements IYouTubeMediaContainer.UseCookies + Protected Const mp3 As String = "mp3" + Private Const aac As String = "aac" + Private Const ac3 As String = "ac3" + Protected PostProcessing_AudioAC3 As Boolean = False + Public Overridable ReadOnly Property Command(ByVal WithCookies As Boolean) As String Implements IYouTubeMediaContainer.Command + Get + If Not File.IsEmptyString Then + If File.Exists Then File = SFile.IndexReindex(File) + Dim cmd$ = String.Empty, formats$ = String.Empty, subs$ = String.Empty, remux$ = String.Empty + _Size = 0 + Height = 0 + Bitrate = 0 + _MediaType = UMTypes.Undefined + If SelectedVideoIndex >= 0 Then + cmd.StringAppend($"bv*[format_id={SelectedVideo.ID}]") + _Size = SelectedVideo.Size + _MediaType = UMTypes.Video + Height = SelectedVideo.Height + _File.Extension = OutputVideoExtension + Else + formats.StringAppend("--extract-audio", " ") + _MediaType = UMTypes.Audio + End If + If SelectedAudioIndex >= 0 Then + Dim atCodec$ + cmd.StringAppend($"ba*[format_id={SelectedAudio.ID}]", "+") + If OutputAudioCodec.StringToLower = ac3 Then + PostProcessing_AudioAC3 = True + formats.StringAppend($"--audio-format {aac}", " ") + atCodec = aac + Else + formats.StringAppend($"--audio-format {OutputAudioCodec.StringToLower}", " ") + atCodec = OutputAudioCodec.StringToLower + End If + If SelectedVideoIndex = -1 Then formats.StringAppend("--add-metadata", " ") + _Size += SelectedAudio.Size + If _MediaType = UMTypes.Undefined Then _MediaType = UMTypes.Audio + Bitrate = SelectedAudio.Bitrate + Dim aCodec$ = SelectedAudio.Codec.StringToLower + If Not aCodec.IsEmptyString AndAlso Not aCodec.StringToLower = atCodec Then + remux.StringAppend($"{aCodec}>{atCodec}") + If SelectedVideoIndex = -1 Then + remux &= $"/{atCodec}" + _File.Extension = atCodec + End If + End If + End If + If SelectedVideoIndex >= 0 And Not SelectedVideo.Extension.StringToLower = OutputVideoExtension.StringToLower Then _ + remux.StringAppend($"{SelectedVideo.Extension.StringToLower}>{OutputVideoExtension.StringToLower}/{OutputVideoExtension.StringToLower}", "/") + If Not remux.IsEmptyString Then formats.StringAppend($"--remux-video ""{remux}""", " ") + If SubtitlesSelectedIndexes.Count > 0 Then + subs = ListAddList(Nothing, Subtitles.Select(Function(s, i) If(SubtitlesSelectedIndexes.Contains(i), s.FullID, String.Empty)), + LAP.NotContainsOnly, EDP.ReturnValue).ListToString(",") + subs = $"--write-subs --write-auto-subs --sub-format {OutputSubtitlesFormat.StringToLower} --sub-langs ""{subs}"" --convert-subs {OutputSubtitlesFormat.StringToLower}" + End If + If Not cmd.IsEmptyString Then + cmd = $"yt-dlp -f ""{cmd}""" + cmd.StringAppend(formats, " ") + cmd.StringAppend(subs, " ") + cmd.StringAppend(YouTubeFunctions.GetCookiesCommand(WithCookies, YouTubeCookieNetscapeFile), " ") + cmd &= $" {URL} -o ""{File.PathWithSeparator}{File.Name}""" + File.Exists(SFO.Path, True) + Return cmd + End If + End If + Return String.Empty + End Get + End Property +#End Region +#Region "Initializer" + Protected Sub New() + Elements = New List(Of IYouTubeMediaContainer) + Thumbnails = New List(Of Thumbnail) + _Subtitles = New List(Of Subtitles) + _SubtitlesDelegated = New List(Of Subtitles) + SubtitlesSelectedIndexes = New List(Of Integer) + MediaObjects = New List(Of MediaObject) + Files = New List(Of SFile) + + PostProcessing_OutputSubtitlesFormats = New List(Of String) + PostProcessing_OutputSubtitlesFormats.ListAddList(MyYouTubeSettings.DefaultSubtitlesFormatAddit) + PostProcessing_OutputAudioFormats = New List(Of String) + PostProcessing_OutputAudioFormats.ListAddList(MyYouTubeSettings.DefaultAudioCodecAddit) + End Sub +#End Region +#Region "ToString, GetHashCode, Equals" + Public NotOverridable Overloads Overrides Function ToString() As String Implements IDownloadableMedia.ToString + Return ToString(False) + End Function + Public Overridable Overloads Function ToString(ByVal ForMediaItem As Boolean) As String Implements IDownloadableMedia.ToString + Return Title + End Function + Public Overrides Function GetHashCode() As Integer + Return $"{ID}.{PlaylistID}.{UserID}.{Title}".GetHashCode + End Function + Public Overrides Function Equals(ByVal Obj As Object) As Boolean + If Not Obj Is Nothing AndAlso Obj.GetType Is Me.GetType Then Return GetHashCode() = Obj.GetHashCode Else Return False + End Function +#End Region +#Region "Delete, UpdateInfoFields, ThrowAny" + Public Overridable Sub Delete(ByVal RemoveFiles As Boolean) Implements IDownloadableMedia.Delete + If CachePath.Exists(SFO.Path, False) Then CachePath.Delete(SFO.Path, SFODelete.DeletePermanently, EDP.None) + If FileSettings.Exists Then FileSettings.Delete(SFO.File, SFODelete.DeletePermanently, EDP.None) + If RemoveFiles Then + Dim fErr As New ErrorsDescriber(EDP.None) + Dim dMode As SFODelete = SFODelete.DeleteToRecycleBin + File.Delete(SFO.File, dMode, fErr) + ThumbnailFile.Delete(SFO.File, dMode, fErr) + If Files.Count > 0 Then Files.ForEach(Sub(f) f.Delete(SFO.File, dMode, fErr)) + End If + If HasElements Then Elements.ForEach(Sub(e) e.Delete(RemoveFiles)) + End Sub + Friend Sub UpdateInfoFields() Implements IYouTubeMediaContainer.UpdateInfoFields + _Size = 0 + If SelectedVideoIndex >= 0 Then _Size += SelectedVideo.Size + If SelectedAudioIndex >= 0 Then _Size += SelectedAudio.Size + If HasElements Then Elements.ForEach(Sub(e) e.UpdateInfoFields()) + End Sub + Protected Sub ThrowAny(ByVal Token As CancellationToken) + Token.ThrowIfCancellationRequested() + If disposedValue Then Throw New ObjectDisposedException(ToString(), "Object disposed") + End Sub +#End Region +#Region "Download" + Private ReadOnly DownloadProgressPattern As RParams = RParams.DMS("\[download\]\s*([\d\.,]+)", 1, EDP.ReturnValue) + Public Property Progress As MyProgress Implements IYouTubeMediaContainer.Progress + Private Property IDownloadableMedia_Progress As Object Implements IDownloadableMedia.Progress + Get + Return Progress + End Get + Set(ByVal p As Object) + If Not p Is Nothing Then + If TypeOf p Is MyProgress Then Progress = p + Else + Progress = Nothing + End If + End Set + End Property + Private Sub DownloadElementsApply() + If HasElements Then + SetElementsSubtitles(Me) + For Each elem As YouTubeMediaContainerBase In Elements + With elem + .OutputAudioCodec = OutputAudioCodec + .OutputSubtitlesFormat = OutputSubtitlesFormat + .OutputVideoExtension = OutputVideoExtension + .PostProcessing_OutputAudioFormats.Clear() + If PostProcessing_OutputAudioFormats.Count > 0 Then .PostProcessing_OutputAudioFormats.AddRange(PostProcessing_OutputAudioFormats) + End With + Next + End If + End Sub + Public Overridable Sub Download(ByVal UseCookies As Boolean, ByVal Token As CancellationToken) Implements IDownloadableMedia.Download + DownloadElementsApply() + If ObjectType = YouTubeMediaType.Single Then + DownloadCommand(UseCookies, Token) + Else + DownloadCommandArray(UseCookies, Token) + End If + RaiseEvent DataDownloaded(Me, Nothing) + End Sub + Private Function DownloadGetElemCountSingle() As Integer + If ObjectType = YouTubeMediaType.Single Then + Return 1 + Else + Return Elements.Sum(Function(e) DirectCast(e, YouTubeMediaContainerBase).DownloadGetElemCountSingle()) + End If + End Function + Protected Sub DownloadCommandArray(ByVal UseCookies As Boolean, ByVal Token As CancellationToken) + Try + If HasElements Then + Dim prExists As Boolean = Not Progress Is Nothing + Dim fDown As EventHandler = Sub(ByVal Sender As Object, ByVal e As EventArgs) + RaiseEvent FileDownloadStarted(Sender, e) + If prExists Then Progress.Perform() + End Sub + If prExists Then + With Progress + .Visible = True + .Value = 0 + .Maximum = DownloadGetElemCountSingle() + .Information = $"Download {ObjectType}" + End With + End If + + Dim cDown As Boolean = False + For Each elem In Elements + With DirectCast(elem, YouTubeMediaContainerBase) + If Not .CoverDownloaded Then .CoverDownloaded = cDown + AddHandler .FileDownloadStarted, fDown + .Download(UseCookies, Token) + cDown = .CoverDownloaded + RemoveHandler .FileDownloadStarted, fDown + End With + If Token.IsCancellationRequested Or disposedValue Then Exit For + Next + + If prExists Then + With Progress + .Value = .Maximum + .Perform(0) + .InformationTemporary = "Download completed" + End With + End If + End If + Catch oex As OperationCanceledException When Token.IsCancellationRequested + Throw oex + Catch dex As ObjectDisposedException When disposedValue + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"YTContainer.DownloadArrayError{ToString()}") + Finally + If Not Token.IsCancellationRequested And Not disposedValue Then + DateDownloaded = Now + MediaState = UMStates.Downloaded + End If + End Try + End Sub + Protected CoverDownloaded As Boolean = False + Private Sub DownloadPlaylistCover(ByVal PlsId As String, ByVal f As SFile, ByVal UseCookies As Boolean) + Try + Dim url$ = $"https://{IIf(IsMusic, "music", "www")}.youtube.com/playlist?list={PlsId}" + Dim r$ + Using resp As New Responser + If UseCookies And MyYouTubeSettings.Cookies.Count > 0 Then resp.Cookies.AddRange(MyYouTubeSettings.Cookies,, EDP.SendToLog) + r = resp.GetResponse(url,, EDP.ReturnValue) + If Not r.IsEmptyString Then + Dim p As RParams = RParams.DM("(?<=https:[\\/]{2,4})[^\.]*[\.]?googleusercontent.com[^\,]+?w(\d+).h(\d+)[^\,]+?(?=\\x22)", 0, RegexReturn.List, EDP.ReturnValue) + Dim l As List(Of String) = RegexReplace(r, p) + If l.ListExists Then l.RemoveAll(Function(uu) uu.IsEmptyString) + If l.ListExists Then + Dim u$ = l.Last + u = u.Replace("\/", "/").TrimStart("/") + Dim position% + Dim ch$ + Do + position = InStr(u, "\") + If position > 0 Then + ch = $"%{Mid(u, position + 2, 2)}" + ch = SymbolsConverter.ASCII.Decode(ch, New ErrorsDescriber(False, False, False, String.Empty)) + u = u.Replace(Mid(u, position, 4), ch) + End If + Loop While position > 0 + url = LinkFormatterSecure(u) + f.Name = "cover" + f.Extension = "jpg" + If resp.DownloadFile(url, f, EDP.ReturnValue) And f.Exists Then CoverDownloaded = True + End If + End If + End Using + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"DownloadPlaylistCover({PlsId}, {f})") + End Try + End Sub + Protected Sub DownloadCommand(ByVal UseCookies As Boolean, ByVal Token As CancellationToken) + Dim dCommand$ = String.Empty + Try + ThrowAny(Token) + If MediaState = UMStates.Downloaded Or Not Checked Then Exit Sub + Dim h As DataReceivedEventHandler = Sub(ByVal Sender As Object, ByVal e As DataReceivedEventArgs) + If Not e.Data.IsEmptyString Then + Dim v# = AConvert(Of Double)(RegexReplace(e.Data, DownloadProgressPattern), NumberProvider, -1) + If v >= 0 Then Progress.Value = v : Progress.Perform(0) + End If + End Sub + RaiseEvent FileDownloadStarted(Me, Nothing) + Using batch As New BatchExecutor(True) With {.Encoding = 65001} + With batch + Dim prExists As Boolean = Not Progress Is Nothing + If prExists Then + AddHandler .OutputDataReceived, h + With Progress + .Visible = True + .Value = 0 + .Maximum = 100 + .Provider = ProgressProvider + .Information = $"Download {MediaType}" + End With + End If + .FileExchanger = MyCache.NewInstance(Of BatchFileExchanger)(CachePath, EDP.ReturnValue) + .FileExchanger.DeleteCacheOnDispose = True + .AddCommand("chcp 65001") + .ChangeDirectory(MyYouTubeSettings.YTDLP.Value) + dCommand = Command(UseCookies) +#If DEBUG Then + Debug.WriteLine(dCommand) +#End If + Task.WaitAll({Task.Run(Sub() .Execute(dCommand))}, Token) + If Token.IsCancellationRequested Then .Kill(EDP.None) + ThrowAny(Token) + If prExists Then + RemoveHandler .OutputDataReceived, h + Progress.Value = 100 + Progress.Perform(0) + End If + If Not File.Exists Then _File.Name = File.File + If File.Exists Then + If PlaylistCount > 0 And Not CoverDownloaded And Not PlaylistID.IsEmptyString Then DownloadPlaylistCover(PlaylistID, File, UseCookies) + If prExists Then Progress.InformationTemporary = $"Download {MediaType}: post processing" + _ThumbnailFile = File + _ThumbnailFile.Name &= "_thumb" + _ThumbnailFile.Extension = "jpg" + If Not ThumbnailUrl.IsEmptyString Then GetWebFile(ThumbnailUrl, _ThumbnailFile, EDP.None) + + ThrowAny(Token) + If MyYouTubeSettings.FFMPEG.Value.Exists Then + .Reset() + .CommandsPermanent.Clear() + .CommandsPermanent.AddRange({"chcp 65001", BatchExecutor.GetDirectoryCommand(MyYouTubeSettings.FFMPEG.Value)}) + .AutoReset = True + Dim files As IEnumerable(Of SFile) + Dim f As SFile + Dim commandFile As SFile + Dim format$ + Dim fPattern$ = $"{File.PathWithSeparator}{File.Name}." & "{0}" + Dim fPatternFiles$ = $"{File.Name}*." & "{0}" + Dim fAacAudio As New SFile(String.Format(fPattern, aac)) + Dim fAc3Audio As New SFile(String.Format(fPattern, ac3)) + Dim aacRequested As Boolean = PostProcessing_OutputAudioFormats.Count > 0 AndAlso + PostProcessing_OutputAudioFormats.Exists(Function(af) af.StringToLower = aac) + Dim ac3Requested As Boolean = PostProcessing_OutputAudioFormats.Count > 0 AndAlso + PostProcessing_OutputAudioFormats.Exists(Function(af) af.StringToLower = ac3) + + ThrowAny(Token) + If PostProcessing_OutputSubtitlesFormats.Count > 0 Then + files = SFile.GetFiles(File, String.Format(fPatternFiles, OutputSubtitlesFormat.StringToLower),, EDP.ReturnValue) + If files.ListExists Then + For Each f In files + For Each format In PostProcessing_OutputSubtitlesFormats + format = format.StringToLower + commandFile = $"{f.PathWithSeparator}{f.Name}.{format}" + Me.Files.Add(commandFile) + ThrowAny(Token) + .Execute($"ffmpeg -i ""{f}"" ""{commandFile}""") + Next + Next + End If + End If + + ThrowAny(Token) + If PostProcessing_OutputAudioFormats.Count > 0 Or PostProcessing_AudioAC3 Then + If Not fAacAudio.Exists Then .Execute($"ffmpeg -i ""{File}"" -vn -acodec {aac} ""{fAacAudio}""") + If PostProcessing_AudioAC3 And Not fAc3Audio.Exists Then + ThrowAny(Token) + .Execute($"ffmpeg -i ""{File}"" -vn -acodec {ac3} ""{fAc3Audio}""") + If Not fAc3Audio.Exists And fAacAudio.Exists Then ThrowAny(Token) : .Execute($"ffmpeg -i ""{fAacAudio}"" -f {ac3} ""{fAc3Audio}""") + End If + If PostProcessing_OutputAudioFormats.Count > 0 Then + For Each format In PostProcessing_OutputAudioFormats + format = format.StringToLower + f = String.Format(fPattern, format) + Me.Files.Add(f) + If Not format = ac3 Or Not f.Exists Then ThrowAny(Token) : .Execute($"ffmpeg -i ""{fAacAudio}"" -f {format} ""{f}""") + Next + End If + End If + + ThrowAny(Token) + If PostProcessing_AudioAC3 Then + f = File + If SelectedVideoIndex >= 0 Then + f.Name &= "tmp00" + Else + f.Extension = ac3 + End If + If Not f.Exists Then ThrowAny(Token) : .Execute($"ffmpeg -i ""{File}"" -i ""{fAc3Audio}"" -c:v copy -c copy -map 0:v:0 -map 1:a:0 ""{f}""") + If f.Exists Then + File.Delete() + If SelectedVideoIndex >= 0 Then SFile.Rename(f, File,, EDP.LogMessageValue) + End If + If fAacAudio.Exists And Not aacRequested Then fAacAudio.Delete() + If fAc3Audio.Exists And Not ac3Requested And SelectedVideoIndex >= 0 Then fAc3Audio.Delete() + End If + End If + End If + End With + End Using + _MediaState = UMStates.Downloaded + Catch oex As OperationCanceledException When Token.IsCancellationRequested + Throw oex + Catch dex As ObjectDisposedException When disposedValue + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"YTContainer.DownloadError: {ToString()}".StringAppendLine(dCommand)) + Finally + If Not Token.IsCancellationRequested And Not disposedValue Then + DateDownloaded = Now + MediaState = UMStates.Downloaded + If Not Progress Is Nothing Then Progress.InformationTemporary = "Download completed" + RaiseEvent FileDownloaded(Me, Nothing) + End If + End Try + End Sub +#End Region +#Region "Load" + Private Sub ApplyElementCheckedValue(ByVal e As EContainer) + If HasElements And e.Count > 0 Then + Dim obj As YouTubeMediaContainerBase + For Each elem As EContainer In e + If Not elem.Value.IsEmptyString Then + obj = GetElementByID(elem.Value, True) + If Not obj Is Nothing Then + If obj.HasElements Then + obj.ApplyElementCheckedValue(elem) + Else + obj.Checked = elem.Attribute(Name_CheckedAttribute).Value.FromXML(Of Boolean)(True) + End If + End If + End If + Next + End If + End Sub + Private Function GetElementByID(ByVal ID As String, Optional ByVal IgnoreCurrentInstance As Boolean = False) As YouTubeMediaContainerBase + If HasElements Then + Dim obj As YouTubeMediaContainerBase + For Each elem As YouTubeMediaContainerBase In Elements + If elem.ID = ID Then + Return elem + Else + obj = elem.GetElementByID(ID) + If Not obj Is Nothing Then Return obj + End If + Next + ElseIf Not IgnoreCurrentInstance And Me.ID = ID Then + Return Me + End If + Return Nothing + End Function + Private Sub IDownloadableMedia_Load(ByVal File As String) Implements IDownloadableMedia.Load + Load(File) + End Sub + Public Overridable Sub Load(ByVal f As SFile) Implements IYouTubeMediaContainer.Load + Try + FileSettings = f + If f.Exists Then + Using x As New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True} + x.LoadData() + Dim fc As SFile = x.Value(Name_CachePath).CSFileP + If fc.Exists(SFO.Path, False) AndAlso SFile.GetFiles(fc, "*.json",, EDP.ReturnValue).Count > 0 Then Parse(Nothing, fc, IsMusic) + XMLPopulateData(Me, x) + _Exists = True + If If(x(Name_CheckedElements)?.Count, 0) > 0 Then ApplyElementCheckedValue(x(Name_CheckedElements)) + If ArrayMaxResolution <> -10 Then SetMaxResolution(ArrayMaxResolution) + End Using + Else + _Exists = False + End If + Catch ex As Exception + _HasError = True + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"YouTubeMediaContainerBase.Load({f})") + End Try + End Sub +#End Region +#Region "Save" + Public Overridable Sub Save() Implements IDownloadableMedia.Save + Try + Dim fSettings As SFile = FileSettings + If fSettings.IsEmptyString Then fSettings = MyCacheSettings.NewFile + Dim f As SFile = fSettings + + If Not MediaState = UMStates.Downloaded Then + If CachePath.Exists(SFO.Path, False) AndAlso Not CachePath.Path.Contains(MyCacheSettings.RootDirectory.Path) Then + f = $"{f.PathWithSeparator}{f.Name}\" + If f.Exists(SFO.Path) Then + Dim files As List(Of SFile) = SFile.GetFiles(CachePath, "*.json", IO.SearchOption.AllDirectories, EDP.ReturnValue) + If files.ListExists Then + CachePath = f + Dim fd As SFile = f + fd.Extension = "json" + For Each f In files + fd.Name = f.Name + SFile.Move(f, fd) + Next + Else + If CachePath.Exists(SFO.Path, False) Then CachePath.Delete(SFO.Path, SFODelete.DeletePermanently, EDP.None) + CachePath = Nothing + End If + End If + End If + Else + If CachePath.Exists(SFO.Path, False) Then CachePath.Delete(SFO.Path, SFODelete.DeletePermanently, EDP.None) + CachePath = Nothing + End If + + Using x As New XmlFile With {.AllowSameNames = True} + fSettings.Extension = "xml" + FileSettings = fSettings + x.AddRange(ToEContainer.Elements) + x.Name = "MediaContainer" + x.Save(fSettings) + End Using + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"YouTubeMediaContainerBase.Save({FileSettings})") + End Try + End Sub +#End Region +#Region "Parse" + Public Overridable Function Parse(ByVal Container As EContainer, ByVal Path As SFile, ByVal IsMusic As Boolean, + Optional ByVal Token As CancellationToken = Nothing, Optional ByVal Progress As IMyProgress = Nothing) As Boolean Implements IYouTubeMediaContainer.Parse + Try + Me.IsMusic = IsMusic + CachePath = Path + If Not Path.IsEmptyString And Not Container Is Nothing Then Throw New InvalidOperationException("Both arguments (Container, Path) are not null") + If Path.IsEmptyString And Container Is Nothing Then Throw New InvalidOperationException("Both arguments (Container, Path) are null") + If Not Path.IsEmptyString AndAlso Not Path.Exists(SFO.File, False) AndAlso Path.Exists(SFO.Path, False) Then + Dim files As List(Of SFile) = SFile.GetFiles(Path,, IO.SearchOption.AllDirectories, EDP.ReturnValue) + If files.Count > 0 Then + If files.Count = 1 Then + Path = files(0) + Else + If ParseFiles(Path, IsMusic, Token, Progress) Then + File = MyYouTubeSettings.OutputPath + Return True + Else + Return False + End If + End If + End If + End If + ThrowAny(Token) + If Not Path.IsEmptyString AndAlso Not Path.File.IsEmptyString AndAlso Path.Exists(SFO.File, False) Then + Dim t$ = Path.GetText(EDP.ReturnValue) + If Not t.IsEmptyString Then Container = JsonDocument.Parse(t, EDP.ReturnValue) + End If + If Not Container Is Nothing Then + With Container + ID = .Value("id") + Title = TitleHtmlConverter.Invoke(.Value("title")) + Description = .Value("description") + URL = .Value("webpage_url") + + PlaylistID = .Value("playlist_id") + PlaylistCount = .Value("n_entries").IfNullOrEmpty(.Value("playlist_count")).FromXML(Of Integer)(0) + PlaylistIndex = .Value("playlist_index").FromXML(Of Integer)(-1) + PlaylistTitle = TitleHtmlConverter.Invoke(.Value("album").IfNullOrEmpty(.Value("playlist_title")).IfNullOrEmpty(.Value("playlist"))) + If Not PlaylistTitle.IsEmptyString And .Value("album").IsEmptyString Then + Dim tmpPls$ = PlaylistTitle.Replace("Album", String.Empty).StringTrimStart(" ", "-") + IsShorts = Not tmpPls.IsEmptyString AndAlso PlaylistTitle.Contains(" - Shorts") + tmpPls = tmpPls.Replace("Shorts", String.Empty).StringTrimStart(" ", "-") + If Not tmpPls.IsEmptyString Then PlaylistTitle = tmpPls + End If + + UserID = .Value("uploader_id") + UserTitle = TitleHtmlConverter.Invoke(.Value("uploader")) + If Not UserTitle.IsEmptyString Then + Dim tmpTitle$ = UserTitle.Replace("Topic", String.Empty).StringTrimEnd(" ", "-") + If Not tmpTitle.IsEmptyString Then UserTitle = tmpTitle + End If + + Dim ext$ = IIf(IsMusic, + MyYouTubeSettings.DefaultAudioCodecMusic.Value.StringToLower, + MyYouTubeSettings.DefaultVideoFormat.Value.StringToLower) + If ext.IsEmptyString Then ext = IIf(IsMusic, "mp3", "mp4") + If Not Title.IsEmptyString Then + _File = $"{Title}.{ext}" + Else + _File.Name = $"{ID}.{ext}" + End If + If Not MyYouTubeSettings.OutputPath.IsEmptyString Then _File.Path = MyYouTubeSettings.OutputPath.Value.Path + File = _File + + If .Contains("duration") Then + Dim tValue%? = AConvert(Of Integer)(.Value("duration"), AModes.Var, Nothing) + If tValue.HasValue Then Duration = TimeSpan.FromSeconds(tValue.Value) + End If + DateAdded = AConvert(Of Date)(.Value("release_date").IfNullOrEmpty(.Value("upload_date")), DateAddedProvider, New Date) + + ParseFormats(.Self) + + ParseThumbnails(.Self) + + ParseSubtitles(.Self) + End With + Return True + End If + If Not Progress Is Nothing Then Progress.Perform() + Return False + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, "[API.YouTube.Objects.YouTubeMediaContainerBase.Parse]") + _HasError = True + Return False + End Try + End Function + Protected Function ParseFiles(ByVal Path As SFile, ByVal IsMusic As Boolean, Optional ByVal Token As CancellationToken = Nothing, + Optional ByVal Progress As IMyProgress = Nothing) As Boolean + Me.IsMusic = IsMusic + CachePath = Path + If Path.Exists(SFO.Path, False) Then + Dim files As List(Of SFile) = SFile.GetFiles(Path,, IO.SearchOption.AllDirectories, EDP.ReturnValue) + If files.Count > 0 Then + Dim progressExists As Boolean = Not Progress Is Nothing + Dim pErr As New ErrorsDescriber(EDP.ReturnValue) + Dim e As EContainer + Dim obj As IYouTubeMediaContainer + Dim t$ + Dim playListDataObtained As Boolean = False + If progressExists Then Progress.Maximum += files.Count + For Each f As SFile In files + t = f.GetText(pErr) + If Not t.IsEmptyString Then + ThrowAny(Token) + e = JsonDocument.Parse(t, pErr) + If Not e Is Nothing Then + If e.Count > 0 Then + If IsMusic Then obj = New Track Else obj = New Video + ThrowAny(Token) + obj.Parse(e, Nothing, IsMusic) + If progressExists Then Progress.Perform() + ThrowAny(Token) + If obj.Exists And Not obj.HasError Then + Duration += obj.Duration + DirectCast(obj, YouTubeMediaContainerBase).CachePath = Path + 'Size += obj.Size + If Not playListDataObtained Then + playListDataObtained = True + With obj + ID = .PlaylistID + Title = .PlaylistTitle + Bitrate = .Bitrate + UserID = .UserID + UserTitle = .UserTitle + End With + End If + Elements.Add(obj) + End If + End If + e.Dispose() + End If + End If + Next + File = MyYouTubeSettings.OutputPath + End If + End If + _Exists = HasElements + Return _Exists + End Function + Protected Sub ParseFormats(ByVal e As EContainer) + Const av As UMTypes = UMTypes.Audio + UMTypes.Video + If If(e({"formats"})?.Count, 0) > 0 Then + Dim obj As MediaObject + Dim nValue# + Dim sValue$ + Dim validCodecValue As Func(Of String, Boolean) = Function(codec) Not codec.IsEmptyString AndAlso Not codec = "none" + + For Each ee In e({"formats"}) + obj = New MediaObject With { + .ID = ee.Value("format_id"), + .URL = ee.Value("url"), + .Extension = ee.Value("ext") + } + obj.Width = AConvert(Of Integer)(ee.Value("width"), NumberProvider, -1) + obj.Height = AConvert(Of Integer)(ee.Value("height"), NumberProvider, -1) + obj.FPS = AConvert(Of Double)(ee.Value("fps"), NumberProvider, -1) + obj.Bitrate = AConvert(Of Double)(ee.Value("tbr"), NumberProvider, -1) + nValue = AConvert(Of Double)(ee.Value("filesize"), NumberProvider, -1) + If nValue > 0 Then obj.Size = (nValue / 1024).RoundVal(2) + sValue = ee.Value("vcodec") + If validCodecValue(sValue) Then + obj.Type = UMTypes.Video + obj.Codec = sValue.Split(".").First + If validCodecValue(ee.Value("acodec")) Then + obj.Type = av + If obj.Size <= 0 Then + nValue = AConvert(Of Double)(ee.Value("filesize_approx"), NumberProvider, -1) + If nValue > 0 Then obj.Size = (nValue / 1024).RoundVal(2) + End If + End If + Else + sValue = ee.Value("acodec") + If validCodecValue(sValue) Then + obj.Type = UMTypes.Audio + obj.Codec = sValue.Split(".").First + obj.Bitrate = AConvert(Of Double)(ee.Value("tbr"), NumberProvider, -1) + Else + Continue For + End If + End If + MediaObjects.Add(obj) + Next + MediaObjects.RemoveAll(Function(m) (m.Type = UMTypes.Video And (m.Width <= 0 Or m.Height <= 0)) Or m.URL.IsEmptyString) + Dim DupRemover As Action(Of UMTypes) = + Sub(ByVal t As UMTypes) + Const webm$ = "webm" + Const avc$ = "avc" + Dim data As New List(Of MediaObject)(MediaObjects.Where(Function(mo) mo.Type = t And mo.Extension = webm)) + If data.Count > 0 Then + Dim d As MediaObject = Nothing + Dim expWebm As Predicate(Of MediaObject) = Function(mo) mo.Extension = webm + Dim expAVC As Predicate(Of MediaObject) = Function(mo) mo.Codec.IfNullOrEmpty("/").ToLower.StartsWith(avc) + Dim comp As Func(Of MediaObject, Predicate(Of MediaObject), Boolean, Boolean) = + Function(mo, exp, isTrue) mo.Type = t And exp.Invoke(mo) = isTrue And mo.Width = d.Width + Dim CountWebm As Func(Of MediaObject, Boolean) = Function(mo) comp.Invoke(mo, expWebm, False) + Dim RemoveWebm As Predicate(Of MediaObject) = Function(mo) comp.Invoke(mo, expWebm, True) + Dim CountAVC As Func(Of MediaObject, Boolean) = Function(mo) comp.Invoke(mo, expAVC, True) + Dim RemoveAVC As Predicate(Of MediaObject) = Function(mo) comp.Invoke(mo, expAVC, False) + For Each d In data + If MediaObjects.Count = 0 Then Exit For + If MediaObjects.LongCount(CountWebm) > 0 Then MediaObjects.RemoveAll(RemoveWebm) + If MediaObjects.Count > 0 AndAlso MediaObjects.LongCount(CountAVC) > 0 Then MediaObjects.RemoveAll(RemoveAVC) + Next + End If + End Sub + If MediaObjects.Count > 0 Then DupRemover.Invoke(UMTypes.Audio) + If MediaObjects.Count > 0 Then DupRemover.Invoke(UMTypes.Video) + If MediaObjects.Count > 0 Then + MediaObjects.Sort() + SelectedAudioIndex = MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Audio) + If SelectedAudioIndex >= 0 Then + Dim aSize# = MediaObjects(SelectedAudioIndex).Size + If aSize > 0 Then + For i% = 0 To MediaObjects.Count - 1 + obj = MediaObjects(i) + If obj.Type = UMTypes.Video Then obj.Size += aSize : MediaObjects(i) = obj + Next + End If + End If + + With MyYouTubeSettings + If Not .DefaultVideoFormat.IsEmptyString Then OutputVideoExtension = .DefaultVideoFormat + PostProcessing_OutputAudioFormats_Reset() + If Not IsMusic Then + If .DefaultVideoDefinition > 0 Then _ + SelectedVideoIndex = MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Video And mo.Height <= .DefaultVideoDefinition) + If SelectedVideoIndex = -1 Then MediaObjects.FindIndex(Function(mo) mo.Type = UMTypes.Video) + If Not .DefaultAudioCodec.IsEmptyString Then OutputAudioCodec = .DefaultAudioCodec + Else + If Not .DefaultAudioCodecMusic.IsEmptyString Then OutputAudioCodec = .DefaultAudioCodecMusic + SelectedVideoIndex = -1 + End If + End With + MediaObjects.ListReindex + End If + End If + End Sub + Protected Sub ParseThumbnails(ByVal e As EContainer) + If If(e({"thumbnails"})?.Count, 0) > 0 Then + Dim thumb As Thumbnail + For Each ee In e({"thumbnails"}) + thumb = New Thumbnail With {.ID = ee.Value("id"), .URL = ee.Value("url")} + thumb.Width = AConvert(Of Integer)(ee.Value("width"), NumberProvider, -1) + thumb.Height = AConvert(Of Integer)(ee.Value("height"), NumberProvider, -1) + If thumb.Width > 0 And thumb.Height > 0 And Not thumb.URL.IsEmptyString Then Thumbnails.Add(thumb) + Next + If Thumbnails.Count > 0 Then + Thumbnails.Sort() + Thumbnails.ListReindex + _ThumbnailUrl = Thumbnails.FirstOrDefault(Function(t) Not t.URL.Contains(".webp")).URL + If _ThumbnailUrl.IsEmptyString Then _ThumbnailUrl = Thumbnails.First.URL + End If + End If + End Sub + Protected Sub ParseSubtitles(ByVal e As EContainer) + Dim subt As Subtitles + Dim ee As EContainer + Dim se As EContainer = e({"subtitles"}) + If If(se?.Count, 0) = 0 OrElse (se.Count = 1 And se(0).Name = "live_chat") Then se = e({"automatic_captions"}) + If If(se?.Count, 0) > 0 Then + If se.Count > 1 OrElse Not se(0).Name = "live_chat" Then + For Each ee In se + subt = New Subtitles With {.ID = ee.Name} + If ee.Count > 0 Then + subt.Name = ee(0).Value("name") + subt.Formats = ee.Select(Function(f) f.Value("ext")).ListToString(",") + End If + If Not subt.ID.IsEmptyString Then _Subtitles.Add(subt) + Next + With MyYouTubeSettings + If Not .DefaultSubtitlesFormat.IsEmptyString Then OutputSubtitlesFormat = .DefaultSubtitlesFormat + If _Subtitles.Count > 0 And .DefaultSubtitles.Count > 0 Then + _Subtitles.Sort() + _Subtitles.ListReindex + SubtitlesSelectedIndexesReset() + PostProcessing_OutputSubtitlesFormats_Reset() + End If + End With + End If + End If + End Sub +#End Region +#Region "IEContainerProvider Support" + Private Function GetElementsChecked() As IEnumerable(Of EContainer) + If HasElements Then + Return Elements.SelectMany(Function(elem As YouTubeMediaContainerBase) elem.GetElementsChecked()) + Else + Return {New EContainer("Element", ID, {New EAttribute(Name_CheckedAttribute, Checked.BoolToInteger)}) With {.AllowSameNames = True}} + End If + End Function + Public Function ToEContainer(Optional ByVal e As ErrorsDescriber = Nothing) As EContainer Implements IEContainerProvider.ToEContainer + Dim c As New EContainer("DataItems") With {.AllowSameNames = True} + c.AddRange(XMLGenerateContainers(Me)) + If HasElements AndAlso Not Elements.All(Function(cc) cc.CheckState = CheckState.Checked) Then + c.Add(New EContainer(Name_CheckedElements) With {.AllowSameNames = True}) + c.Last.AddRange(Of EContainer)(GetElementsChecked()) + End If + Return ToEContainer_Addit(c) + End Function + Protected Overridable Function ToEContainer_Addit(ByRef Container As EContainer) As EContainer + Return Container + End Function +#End Region +#Region "IComparable Support" + Protected Overridable Function CompareTo(ByVal Other As IYouTubeMediaContainer) As Integer Implements IComparable(Of IYouTubeMediaContainer).CompareTo + If CInt(ObjectType).CompareTo(CInt(Other.ObjectType)) = 0 Then + Select Case ObjectType + Case YouTubeMediaType.PlayList + If DateAdded.CompareTo(Other.DateAdded) = 0 Then + If Not PlaylistTitle.IsEmptyString AndAlso Not Other.PlaylistTitle.IsEmptyString Then + Return PlaylistTitle.CompareTo(Other.PlaylistTitle) + Else + Return 0 + End If + Else + Return DateAdded.CompareTo(Other.DateAdded) + End If + Case YouTubeMediaType.Single + If PlaylistIndex.CompareTo(Other.PlaylistIndex) = 0 Then + If Not Title.IsEmptyString And Not Other.Title.IsEmptyString Then + Return Title.CompareTo(Other.Title) + Else + Return 0 + End If + Else + Return PlaylistIndex.CompareTo(Other.PlaylistIndex) + End If + Case YouTubeMediaType.Channel + If Not Title.IsEmptyString And Not Other.Title.IsEmptyString Then + Return Title.CompareTo(Other.Title) + Else + Return 0 + End If + Case Else : Return 0 + End Select + Else + Return CInt(ObjectType).CompareTo(CInt(Other.ObjectType)) + End If + End Function +#End Region +#Region "IDisposable Support" + Protected disposedValue As Boolean = False + Public ReadOnly Property IsDisposed As Boolean Implements IYouTubeMediaContainer.IsDisposed + Get + Return disposedValue + End Get + End Property + Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean) + If Not disposedValue Then + If disposing Then + Elements.ListClearDispose + Thumbnails.Clear() + _Subtitles.Clear() + _SubtitlesDelegated.Clear() + SubtitlesSelectedIndexes.Clear() + MediaObjects.Clear() + Files.Clear() + PostProcessing_OutputAudioFormats.Clear() + PostProcessing_OutputSubtitlesFormats.Clear() + End If + disposedValue = True + End If + End Sub + Protected NotOverridable Overrides Sub Finalize() + Dispose(False) + MyBase.Finalize() + End Sub + Public Overloads Sub Dispose() Implements IDisposable.Dispose + Dispose(True) + GC.SuppressFinalize(Me) + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler.YouTube/SCrawler.YouTube.vbproj b/SCrawler.YouTube/SCrawler.YouTube.vbproj new file mode 100644 index 0000000..8cc5d22 --- /dev/null +++ b/SCrawler.YouTube/SCrawler.YouTube.vbproj @@ -0,0 +1,318 @@ + + + + + Debug + AnyCPU + {7C764707-7FD1-469C-A365-94605C193607} + Library + + + SCrawler + SCrawler.YouTube + 512 + Windows + v4.6.1 + true + true + + + AnyCPU + true + full + true + true + bin\Debug\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + AnyCPU + pdbonly + false + true + true + bin\Release\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + On + + + Binary + + + Off + + + On + + + true + true + true + bin\x64\Debug\ + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + full + x64 + true + + + true + bin\x64\Release\ + true + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + pdbonly + x64 + + + true + true + true + bin\x86\Debug\ + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + full + x86 + true + + + true + bin\x86\Release\ + true + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + pdbonly + x86 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PlayListParserForm.vb + + + Form + + + + + MediaItem.vb + + + UserControl + + + MusicPlaylistsForm.vb + + + Form + + + ParsingProgressForm.vb + + + Form + + + PlaylistArrayForm.vb + + + Form + + + VideoOption.vb + + + UserControl + + + + + + + + + True + Application.myapp + True + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + + True + True + SiteYouTube.resx + + + + + VideoListForm.vb + + + Form + + + VideoOptionsForm.vb + + + Form + + + + + + + PlayListParserForm.vb + + + MediaItem.vb + + + MusicPlaylistsForm.vb + + + ParsingProgressForm.vb + + + PlaylistArrayForm.vb + + + VideoOption.vb + + + PublicVbMyResourcesResXFileCodeGenerator + Resources.Designer.vb + My.Resources + Designer + + + My.Resources + PublicResXFileCodeGenerator + SiteYouTube.Designer.vb + + + VideoListForm.vb + + + VideoOptionsForm.vb + + + + + + MyApplicationCodeGenerator + Application.Designer.vb + + + SettingsSingleFileGenerator + My + Settings.Designer.vb + + + + + + {fc532253-1ab3-4def-a28a-dfdd9a481eb2} + PersonalUtilities.Notifications + + + {8405896b-2685-4916-bc93-1fb514c323a9} + PersonalUtilities + + + {d4650f6b-5a54-44b6-999b-6c675b7116b1} + SCrawler.PluginProvider + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SCrawler.YouTube/SiteYouTube.Designer.vb b/SCrawler.YouTube/SiteYouTube.Designer.vb new file mode 100644 index 0000000..94c0ae9 --- /dev/null +++ b/SCrawler.YouTube/SiteYouTube.Designer.vb @@ -0,0 +1,107 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + +Imports System + +Namespace My.Resources + + 'This class was auto-generated by the StronglyTypedResourceBuilder + 'class via a tool like ResGen or Visual Studio. + 'To add or remove a member, edit your .ResX file then rerun ResGen + 'with the /str option, or rebuild your VS project. + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + _ + Public Class SiteYouTube + + Private Shared resourceMan As Global.System.Resources.ResourceManager + + Private Shared resourceCulture As Global.System.Globalization.CultureInfo + + _ + Friend Sub New() + MyBase.New + End Sub + + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + _ + Public Shared ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager + Get + If Object.ReferenceEquals(resourceMan, Nothing) Then + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("SCrawler.SiteYouTube", GetType(SiteYouTube).Assembly) + resourceMan = temp + End If + Return resourceMan + End Get + End Property + + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + _ + Public Shared Property Culture() As Global.System.Globalization.CultureInfo + Get + Return resourceCulture + End Get + Set + resourceCulture = value + End Set + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + ''' + Public Shared ReadOnly Property YouTubeIcon_32() As System.Drawing.Icon + Get + Dim obj As Object = ResourceManager.GetObject("YouTubeIcon_32", resourceCulture) + Return CType(obj,System.Drawing.Icon) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + ''' + Public Shared ReadOnly Property YouTubeMusicIcon_32() As System.Drawing.Icon + Get + Dim obj As Object = ResourceManager.GetObject("YouTubeMusicIcon_32", resourceCulture) + Return CType(obj,System.Drawing.Icon) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public Shared ReadOnly Property YouTubeMusicPic_96() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("YouTubeMusicPic_96", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Bitmap. + ''' + Public Shared ReadOnly Property YouTubePic_96() As System.Drawing.Bitmap + Get + Dim obj As Object = ResourceManager.GetObject("YouTubePic_96", resourceCulture) + Return CType(obj,System.Drawing.Bitmap) + End Get + End Property + End Class +End Namespace diff --git a/SCrawler.YouTube/SiteYouTube.resx b/SCrawler.YouTube/SiteYouTube.resx new file mode 100644 index 0000000..ebeb740 --- /dev/null +++ b/SCrawler.YouTube/SiteYouTube.resx @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Content\Icons\YouTubeIcon_32.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Content\Icons\YouTubeMusicIcon_32.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Content\Pictures\YouTubeMusicPic_96.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + Content\Pictures\YouTubePic_96.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/.editorconfig b/SCrawler.YouTubeDownloader/.editorconfig new file mode 100644 index 0000000..18ddd08 --- /dev/null +++ b/SCrawler.YouTubeDownloader/.editorconfig @@ -0,0 +1,3 @@ +[*.vb] +# Modifier preferences +file_header_template = Copyright (C) 2023 Andy https://github.com/AAndyProgram\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program. If not, see \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/App.config b/SCrawler.YouTubeDownloader/App.config new file mode 100644 index 0000000..5534e28 --- /dev/null +++ b/SCrawler.YouTubeDownloader/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/Content/Icons/ArrowDownIcon_Orange_24.ico b/SCrawler.YouTubeDownloader/Content/Icons/ArrowDownIcon_Orange_24.ico new file mode 100644 index 0000000..81b7259 Binary files /dev/null and b/SCrawler.YouTubeDownloader/Content/Icons/ArrowDownIcon_Orange_24.ico differ diff --git a/SCrawler.YouTubeDownloader/Content/Icons/RainbowIcon_48.ico b/SCrawler.YouTubeDownloader/Content/Icons/RainbowIcon_48.ico new file mode 100644 index 0000000..47e29f8 Binary files /dev/null and b/SCrawler.YouTubeDownloader/Content/Icons/RainbowIcon_48.ico differ diff --git a/SCrawler.YouTubeDownloader/MainFrame.Designer.vb b/SCrawler.YouTubeDownloader/MainFrame.Designer.vb new file mode 100644 index 0000000..bc7fecb --- /dev/null +++ b/SCrawler.YouTubeDownloader/MainFrame.Designer.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 + +Partial Public Class MainFrame : Inherits SCrawler.DownloadObjects.STDownloader.VideoListForm + + Protected Overrides Sub Dispose(ByVal disposing As Boolean) + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + MyBase.Dispose(disposing) + End Sub + Private components As System.ComponentModel.IContainer + + Private Sub InitializeComponent() + Me.components = New System.ComponentModel.Container() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MainFrame)) + Me.TRAY_ICON = New System.Windows.Forms.NotifyIcon(Me.components) + Me.TRAY_CONTEXT = New System.Windows.Forms.ContextMenuStrip(Me.components) + Me.BTT_TRAY_CLOSE = New System.Windows.Forms.ToolStripMenuItem() + Me.TRAY_CONTEXT.SuspendLayout() + Me.SuspendLayout() + ' + 'TRAY_ICON + ' + Me.TRAY_ICON.BalloonTipIcon = System.Windows.Forms.ToolTipIcon.Info + Me.TRAY_ICON.BalloonTipTitle = "YouTube Downloader" + Me.TRAY_ICON.ContextMenuStrip = Me.TRAY_CONTEXT + Me.TRAY_ICON.Icon = CType(resources.GetObject("TRAY_ICON.Icon"), System.Drawing.Icon) + Me.TRAY_ICON.Text = "YouTube Downloader" + ' + 'TRAY_CONTEXT + ' + Me.TRAY_CONTEXT.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.BTT_TRAY_CLOSE}) + Me.TRAY_CONTEXT.Name = "ContextMenuStrip1" + Me.TRAY_CONTEXT.Size = New System.Drawing.Size(181, 48) + ' + 'BTT_TRAY_CLOSE + ' + Me.BTT_TRAY_CLOSE.Image = CType(resources.GetObject("BTT_TRAY_CLOSE.Image"), System.Drawing.Image) + Me.BTT_TRAY_CLOSE.Name = "BTT_TRAY_CLOSE" + Me.BTT_TRAY_CLOSE.Size = New System.Drawing.Size(180, 22) + Me.BTT_TRAY_CLOSE.Text = "Close" + ' + 'MainFrame + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.ClientSize = New System.Drawing.Size(1008, 729) + Me.Name = "MainFrame" + Me.TRAY_CONTEXT.ResumeLayout(False) + Me.ResumeLayout(False) + Me.PerformLayout() + + End Sub + + Private WithEvents TRAY_ICON As NotifyIcon + Private WithEvents TRAY_CONTEXT As ContextMenuStrip + Private WithEvents BTT_TRAY_CLOSE As ToolStripMenuItem +End Class \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/MainFrame.resx b/SCrawler.YouTubeDownloader/MainFrame.resx new file mode 100644 index 0000000..cbd1fc6 --- /dev/null +++ b/SCrawler.YouTubeDownloader/MainFrame.resx @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 310, 17 + + + 425, 17 + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29m + dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAVoSURBVEhLhZVrTJNXGMdfrtNSQIoadKRz2o0CorU3 + WkDIVBRaaGNbwAteh+AARRQlitEYTTRekiX7sH3YPmyZH9wtziybigLRCWTaCW5sCBWhlrb0Ci9zSxbo + 2f+UliGX7SS/tO85z/k9T57zXhhCCPO7Wh3VIhB83JKQ0Nu4bNlHm5YseZ1hmHC69n+Y5HLFcz7/ft/S + pY+vr1hhwL4oEBJcZ0x793If5uZ+1VNfT/qvXCHP6+p8tzMymqRxcW8hMGKqbDo9MlmWddu2AfbiRTJ6 + +TIZKC52fyAUVi2JiYkLJmGaBYIPnx4+TPrOnCH9p08TC4LNx46RWwrF/ZXR0W/PleRZZuY669atZvbS + JcJiL9vQQEZPnSKmwkLPjcTE97GPB8KZlvh4C5X31dWRgRMniAVBtvPnyWB9ve+2XP7jmtjYpOlJTOnp + G60lJRZaOZWPQs4ePUpGUZh3xw7SnJDQhT0KEM3c5fOv9paVkX4kMAPL8ePEig1D584RG9rVpFS2rY6J + EQaTmKTSjbbiYsvIhQuERTGjKIrFvtHaWjK8fz9plsudexYu/BLxKsBj9ALBGzel0vt9e/b4XiBoENhQ + zRDOxIWWOY4cIS0KRZs4Nja5QyLJtRoM1pGzZ/0tYVExi/ayNTVkBPJ76enuJA7nM4j3gVWAHjgTIYqL + E96SStvMu3YR64EDxF5dTYYOHSJOJPNA5Kiu9rUrlZ1mrdbCnjzpr5jFGotYtqpqQi6TuVM4nKvwlYHU + gDzU31OMSGl8fPJtsbjVsn27z15RQRzAVVlJ3BB4kcx78CAZQbUjVIxrFtd+OdrbmpHhEXG5VE4rTwHz + wMRdFDw4jEgFj5dyRyRqsxYVEcfu3cQFPPv2ES8qHEbCYRzgsFZLvO+8Q7xKJXGDVoXCK46Ovob95YBW + Ph/8+xwE/wSTyHi81OZVq9qsGs2Ye8sW4srPJy6JhDgTE4kzOpo4IyKIMyyMOLhcX9Py5R4lj0cPtAKs + BBwwKfc7p174J5BEhHY9FIk6bBDaIRuiQkDFfsLDSbdU+pdBKPwe8e+BNDBD7vdNn6BYd+6stK5da7bP + nz9TDujcoEAw1lJY+CyFz9dCHDubnDJjwltRccS5fr3TjurnlIMBYE5NJY8Nhq7SrCwREsz6xL9y4S4v + b3Bt2uSyR0XNkDvQe9ouKu8HvaGh5FfQIxL5OgyG30qUStqmGUkm/3jKy0+48vLcs1XuiI8nL/Ly/rYl + JfmovCcgN4JW+l8iGe8oKuoqzcyckSQob3CpVB47l+sXv9KWxYtJt0r1x9ns7HZjQYHNnJxMfoH0EXgA + 7oFm0CmTjRsNhs6Na9bQF+Tkq57xlJXVu9Rqz9Bs8kWLSG9BwcsqieQONlXnpaaWdul0z7rR+6C8CTSC + m8Aol4+36/XGT7VaevCRIIRx6/WWoQULZq2cyveLxY0IrAT0IHm1OTmZT3Q6U2da2qT8B/Ad+BZ05OSM + GXW6p4hdBiIZZ1FRt5vPn6vyuwiqCsj9Xyq6qXbDBkWnXm/6OS3NN1X+dUgIeZSdPXZPoxlEXC6IY9pL + S7faNBqXC9Iplf95YBb5ZF+RpGbdunQcbO/D1avJ9YC8LT19/Iv8/BeqpKRPEDORAGNeY3HxSYtG43Eq + FL5etfpljUzWhPlZ5VOTlGVliR+hHUbs+0mpHP9GpRqM5XAuY20zmGgRRohYKIx9rNd/3qfTOa7l5uLu + C63BvARw6fp0eRCMyBslJe8+2bx58EFhoVMlFNJvgQ4kgggQEgykvV0ApEAd+J3z8Z8KxmuA3pr0zikA + b4LJZ2FqYBigFdOPNf0NC679Fxi0OPr+XxiAJgwURph/AJfOQQebMR8TAAAAAElFTkSuQmCC + + + + + AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQAAAAABAABUBQAA + rAwAALISAgLLFxER/RsjI/8gLi7/JDQ0/yc0NP8nNDT/JzQ0/yc0NP8nNDT/JzQ0/yc0NP8nKyv/IyEh + /yAPD/0bAgLLFgAAshIAAKwNAABTBgAAAgEAABcAAAAAAAAAAAAAAAAAAAAAAAAA8QAAANUUAADpWgAA + 8ZEAAPSvAAD0vwAA8csAAPTTAQH/2QMD/94DA//iBAT/5QQE/+UEBP/lBAT/5QQE/+UEBP/lBAT/5QQE + /+UDA//hAgL/3gEB/9gAAPTTAADxywAA88AAAPOwAAD0lgAA7GQAAOUbAAD/AAAAUQAAAP4AAADxKQAA + +bYAAP76AAD+/wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD+/AAA+scAAOk5AAD/AAAA + xxEAAPivAAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + +cQAAN4cAADlSAAA/PAAAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD++AAA6mAAAOl2AAD+/QAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP7/AADrkgAA6JYAAP7/AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wMD//8GBv//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAPW0AAD0rAAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//HR3//4uL//81Nf//AgL//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA88sAAPS+AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8hIf//39///+Tk//94eP//Fxf//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD12QAA7s0AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//yEh///e3v////////v7///Bwf//TEz//wYG + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAPLkAADt0gAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//ISH//97e//////////////// + ///v7///l5f//ygo//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + 8OsAAO/SAAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8hIf//3t7///// + ////////////////////////2tr//11d//8CAv//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AADy7AAA7dIAAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//yEh + ///e3v////////////////////////v7///Bwf//SEj//wIC//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAPHsAADt0gAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//ISH//97e///////////////////g4P//dnb//xUV//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA8ekAAO/LAAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8hIf//3t7////////09P//pKT//zEx//8BAf//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AADz4QAA9bwAAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//yEh///e3v//zs7//1lZ//8KCv//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAPTVAADxqAAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//GRn//2xs//8fH///AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA88UAAOiRAAD+/wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8BAf//AgL//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AADzqwAA + 6XAAAP78AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + /v8AAOuHAADiPwAA++sAAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD88wAA5FIAALoKAAD1nAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAPi0AADJEwAA7gAAAN8aAAD0mgAA/O4AAP7/AAD+/wAA//8AAP//AAD//wAA + //8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA + //8AAP//AAD+/wAA/v8AAPz1AAD5tAAA7isAAP8AAAAAAAAA5AAAAMMIAADmOgAA6GwAAOeJAQH4nAQE + +KsGBva1CAj+vAoK/8ILC//GDAz/ywwM/8sMDP/LDAz/ywwM/8sMDP/LDAz/ywsL/8kKCv/ECQn/wAcH + /LkFBfayAwP5qAEB9ZoAAOaIAADnbwAA5UYAANARAADqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExM + aABNTWYBQ0N2BEBAnQdAQPcLQED/DkBA/xBAQP8SQED/EkBA/xJAQP8SQED/EkBA/xJAQP8SQED/EUBA + /w9AQP8NQEDiCkBAhAZGRnQDVlZVAVNTWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA//////////////////////AAAA/AAAADgAAAAQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAA + AAHAAAAD/AAAP/////////////////////8= + + + \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/MainFrame.vb b/SCrawler.YouTubeDownloader/MainFrame.vb new file mode 100644 index 0000000..edd400e --- /dev/null +++ b/SCrawler.YouTubeDownloader/MainFrame.vb @@ -0,0 +1,87 @@ +' 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.ComponentModel +Imports SCrawler.API.YouTube +Imports PersonalUtilities.Forms +Public Class MainFrame + Private WithEvents MyActivator As FormActivator + Public Sub New() + InitializeComponent() + AppMode = True + MyActivator = New FormActivator(Me, TRAY_ICON) + End Sub + Protected Overrides Sub VideoListForm_Load(sender As Object, e As EventArgs) + MyBase.VideoListForm_Load(sender, e) + TRAY_ICON.Visible = MyYouTubeSettings.CloseToTray + End Sub + Private _CloseInvoked As Boolean = False + Private _IgnoreTrayOptions As Boolean = False + Private _IgnoreCloseConfirm As Boolean = False + Protected Overrides Async Sub VideoListForm_Closing(sender As Object, e As CancelEventArgs) + If MyYouTubeSettings.CloseToTray And Not _IgnoreTrayOptions Then + e.Cancel = True + Hide() + Else + If CheckForClose(_IgnoreCloseConfirm) Then + If _CloseInvoked Then GoTo CloseContinue + If MyJob.Working Then + If MsgBoxE({"The program is still downloading something..." & vbNewLine & + "Are you sure you want to stop downloading and exit the program?", + "Downloading in progress"}, + MsgBoxStyle.Exclamation,,, + {"Stop downloading and close", "Cancel"}) = 0 Then + _CloseInvoked = True + e.Cancel = True + MyJob.Cancel() + Await Task.Run(Sub() + While MyJob.Working : Threading.Thread.Sleep(500) : End While + End Sub) + End If + End If + Else + GoTo DropCloseParams + End If + GoTo CloseContinue +DropCloseParams: + e.Cancel = True + _IgnoreTrayOptions = False + _IgnoreCloseConfirm = False + _CloseInvoked = False + Exit Sub +CloseContinue: + If _CloseInvoked Then Close() +CloseResume: + End If + End Sub + Protected Overrides Sub VideoListForm_Disposed(sender As Object, e As EventArgs) + MyActivator.Dispose() + MyBase.VideoListForm_Disposed(sender, e) + End Sub + Private Sub BTT_TRAY_CLOSE_Click(sender As Object, e As EventArgs) Handles BTT_TRAY_CLOSE.Click + If CheckForClose(False) Then _IgnoreCloseConfirm = True : _IgnoreTrayOptions = True : Close() + End Sub + Private Function CheckForClose(ByVal _Ignore As Boolean) As Boolean + If MyYouTubeSettings.ExitConfirm And Not _Ignore Then + Return MsgBoxE({"Do you want to close the program?", "Closing the program"}, MsgBoxStyle.YesNo) = MsgBoxResult.Yes + Else + Return True + End If + End Function + Protected Overrides Sub BTT_SETTINGS_Click(sender As Object, e As EventArgs) + MyBase.BTT_SETTINGS_Click(sender, e) + TRAY_ICON.Visible = MyYouTubeSettings.CloseToTray + End Sub + Protected Overrides Sub MyJob_Started(ByVal Sender As Object, ByVal e As EventArgs) + TRAY_ICON.Icon = My.Resources.ArrowDownIcon_Orange_24 + End Sub + Protected Overrides Sub MyJob_Finished(ByVal Sender As Object, ByVal e As EventArgs) + TRAY_ICON.Icon = My.Resources.SiteYouTube.YouTubeIcon_32 + MyBase.MyJob_Finished(Sender, e) + End Sub +End Class \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/My Project/Application.Designer.vb b/SCrawler.YouTubeDownloader/My Project/Application.Designer.vb new file mode 100644 index 0000000..39d65f1 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Application.Designer.vb @@ -0,0 +1,38 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My + + 'NOTE: This file is auto-generated; do not modify it directly. To make changes, + ' or if you encounter build errors in this file, go to the Project Designer + ' (go to Project Properties or double-click the My Project node in + ' Solution Explorer), and make changes on the Application tab. + ' + Partial Friend Class MyApplication + + _ + Public Sub New() + MyBase.New(Global.Microsoft.VisualBasic.ApplicationServices.AuthenticationMode.Windows) + Me.IsSingleInstance = false + Me.EnableVisualStyles = true + Me.SaveMySettingsOnExit = true + Me.ShutDownStyle = Global.Microsoft.VisualBasic.ApplicationServices.ShutdownMode.AfterMainFormCloses + End Sub + + _ + Protected Overrides Sub OnCreateMainForm() + Me.MainForm = Global.SCrawler.MainFrame + End Sub + End Class +End Namespace diff --git a/SCrawler.YouTubeDownloader/My Project/Application.myapp b/SCrawler.YouTubeDownloader/My Project/Application.myapp new file mode 100644 index 0000000..bd96f73 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Application.myapp @@ -0,0 +1,10 @@ + + + true + MainFrame + false + 0 + true + 0 + true + \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/My Project/AssemblyInfo.vb b/SCrawler.YouTubeDownloader/My Project/AssemblyInfo.vb new file mode 100644 index 0000000..b9a296a --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/AssemblyInfo.vb @@ -0,0 +1,37 @@ +Imports System.Resources +Imports System +Imports System.Reflection +Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. + +' Review the values of the assembly attributes + + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: +' + + + + diff --git a/SCrawler.YouTubeDownloader/My Project/Resources.Designer.vb b/SCrawler.YouTubeDownloader/My Project/Resources.Designer.vb new file mode 100644 index 0000000..941fb36 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Resources.Designer.vb @@ -0,0 +1,83 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + +Imports System + +Namespace My.Resources + + 'This class was auto-generated by the StronglyTypedResourceBuilder + 'class via a tool like ResGen or Visual Studio. + 'To add or remove a member, edit your .ResX file then rerun ResGen + 'with the /str option, or rebuild your VS project. + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + _ + Friend Module Resources + + Private resourceMan As Global.System.Resources.ResourceManager + + Private resourceCulture As Global.System.Globalization.CultureInfo + + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + _ + Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager + Get + If Object.ReferenceEquals(resourceMan, Nothing) Then + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("SCrawler.Resources", GetType(Resources).Assembly) + resourceMan = temp + End If + Return resourceMan + End Get + End Property + + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + _ + Friend Property Culture() As Global.System.Globalization.CultureInfo + Get + Return resourceCulture + End Get + Set + resourceCulture = value + End Set + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + ''' + Friend ReadOnly Property ArrowDownIcon_Orange_24() As System.Drawing.Icon + Get + Dim obj As Object = ResourceManager.GetObject("ArrowDownIcon_Orange_24", resourceCulture) + Return CType(obj,System.Drawing.Icon) + End Get + End Property + + ''' + ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + ''' + Friend ReadOnly Property RainbowIcon_48() As System.Drawing.Icon + Get + Dim obj As Object = ResourceManager.GetObject("RainbowIcon_48", resourceCulture) + Return CType(obj,System.Drawing.Icon) + End Get + End Property + End Module +End Namespace diff --git a/SCrawler.YouTubeDownloader/My Project/Resources.resx b/SCrawler.YouTubeDownloader/My Project/Resources.resx new file mode 100644 index 0000000..3a89741 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Content\Icons\ArrowDownIcon_Orange_24.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Content\Icons\RainbowIcon_48.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/SCrawler.YouTubeDownloader/My Project/Settings.Designer.vb b/SCrawler.YouTubeDownloader/My Project/Settings.Designer.vb new file mode 100644 index 0000000..fcfd812 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Settings.Designer.vb @@ -0,0 +1,73 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My + + _ + Partial Friend NotInheritable Class MySettings + Inherits Global.System.Configuration.ApplicationSettingsBase + + Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings()),MySettings) + +#Region "My.Settings Auto-Save Functionality" +#If _MyType = "WindowsForms" Then + Private Shared addedHandler As Boolean + + Private Shared addedHandlerLockObject As New Object + + _ + Private Shared Sub AutoSaveSettings(sender As Global.System.Object, e As Global.System.EventArgs) + If My.Application.SaveMySettingsOnExit Then + My.Settings.Save() + End If + End Sub +#End If +#End Region + + Public Shared ReadOnly Property [Default]() As MySettings + Get + +#If _MyType = "WindowsForms" Then + If Not addedHandler Then + SyncLock addedHandlerLockObject + If Not addedHandler Then + AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings + addedHandler = True + End If + End SyncLock + End If +#End If + Return defaultInstance + End Get + End Property + End Class +End Namespace + +Namespace My + + _ + Friend Module MySettingsProperty + + _ + Friend ReadOnly Property Settings() As Global.SCrawler.My.MySettings + Get + Return Global.SCrawler.My.MySettings.Default + End Get + End Property + End Module +End Namespace diff --git a/SCrawler.YouTubeDownloader/My Project/Settings.settings b/SCrawler.YouTubeDownloader/My Project/Settings.settings new file mode 100644 index 0000000..85b890b --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SCrawler.YouTubeDownloader/My Project/app.manifest b/SCrawler.YouTubeDownloader/My Project/app.manifest new file mode 100644 index 0000000..9ce67d2 --- /dev/null +++ b/SCrawler.YouTubeDownloader/My Project/app.manifest @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SCrawler.YouTubeDownloader/SCrawler.YouTubeDownloader.vbproj b/SCrawler.YouTubeDownloader/SCrawler.YouTubeDownloader.vbproj new file mode 100644 index 0000000..d15d2d8 --- /dev/null +++ b/SCrawler.YouTubeDownloader/SCrawler.YouTubeDownloader.vbproj @@ -0,0 +1,198 @@ + + + + + Debug + AnyCPU + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF} + WinExe + SCrawler.My.MyApplication + SCrawler + YouTubeDownloader + 512 + WindowsForms + v4.6.1 + true + true + + + AnyCPU + true + full + true + true + bin\Debug\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + false + + + AnyCPU + pdbonly + false + true + true + bin\Release\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + false + + + On + + + Binary + + + Off + + + On + + + Content\Icons\RainbowIcon_48.ico + + + true + true + true + bin\x64\Debug\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + full + x64 + true + + + true + bin\x64\Release\ + + + true + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + pdbonly + x64 + true + + + true + true + true + bin\x86\Debug\ + + + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + full + x86 + true + + + true + bin\x86\Release\ + + + true + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + pdbonly + x86 + true + + + My Project\app.manifest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MainFrame.vb + + + Form + + + + True + Application.myapp + True + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + MainFrame.vb + + + VbMyResourcesResXFileCodeGenerator + Resources.Designer.vb + My.Resources + Designer + + + + + + + MyApplicationCodeGenerator + Application.Designer.vb + + + SettingsSingleFileGenerator + My + Settings.Designer.vb + + + + + + {8405896b-2685-4916-bc93-1fb514c323a9} + PersonalUtilities + + + {7c764707-7fd1-469c-a365-94605c193607} + SCrawler.YouTube + + + + + + + + + + \ No newline at end of file diff --git a/SCrawler.sln b/SCrawler.sln index cee7b98..5f6a042 100644 --- a/SCrawler.sln +++ b/SCrawler.sln @@ -12,13 +12,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore Changelog.md = Changelog.md README.md = README.md - ToDo.txt = ToDo.txt EndProjectSection EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "SCrawler.PluginProvider", "SCrawler.PluginProvider\SCrawler.PluginProvider.vbproj", "{D4650F6B-5A54-44B6-999B-6C675B7116B1}" EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "PersonalUtilities.Notifications", "..\..\MyUtilities\PersonalUtilities.Notifications\PersonalUtilities.Notifications.vbproj", "{FC532253-1AB3-4DEF-A28A-DFDD9A481EB2}" EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "SCrawler.YouTube", "SCrawler.YouTube\SCrawler.YouTube.vbproj", "{7C764707-7FD1-469C-A365-94605C193607}" +EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "SCrawler.YouTubeDownloader", "SCrawler.YouTubeDownloader\SCrawler.YouTubeDownloader.vbproj", "{3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,6 +80,30 @@ Global {FC532253-1AB3-4DEF-A28A-DFDD9A481EB2}.Release|x64.Build.0 = Release|x64 {FC532253-1AB3-4DEF-A28A-DFDD9A481EB2}.Release|x86.ActiveCfg = Release|x86 {FC532253-1AB3-4DEF-A28A-DFDD9A481EB2}.Release|x86.Build.0 = Release|x86 + {7C764707-7FD1-469C-A365-94605C193607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C764707-7FD1-469C-A365-94605C193607}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C764707-7FD1-469C-A365-94605C193607}.Debug|x64.ActiveCfg = Debug|x64 + {7C764707-7FD1-469C-A365-94605C193607}.Debug|x64.Build.0 = Debug|x64 + {7C764707-7FD1-469C-A365-94605C193607}.Debug|x86.ActiveCfg = Debug|x86 + {7C764707-7FD1-469C-A365-94605C193607}.Debug|x86.Build.0 = Debug|x86 + {7C764707-7FD1-469C-A365-94605C193607}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C764707-7FD1-469C-A365-94605C193607}.Release|Any CPU.Build.0 = Release|Any CPU + {7C764707-7FD1-469C-A365-94605C193607}.Release|x64.ActiveCfg = Release|x64 + {7C764707-7FD1-469C-A365-94605C193607}.Release|x64.Build.0 = Release|x64 + {7C764707-7FD1-469C-A365-94605C193607}.Release|x86.ActiveCfg = Release|x86 + {7C764707-7FD1-469C-A365-94605C193607}.Release|x86.Build.0 = Release|x86 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|x64.ActiveCfg = Debug|x64 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|x64.Build.0 = Debug|x64 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|x86.ActiveCfg = Debug|x86 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Debug|x86.Build.0 = Debug|x86 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|Any CPU.Build.0 = Release|Any CPU + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|x64.ActiveCfg = Release|x64 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|x64.Build.0 = Release|x64 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|x86.ActiveCfg = Release|x86 + {3F2F2C29-4ADB-48B5-A66E-EE0F51D0DCEF}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SCrawler/API/Base/Declarations.vb b/SCrawler/API/Base/Declarations.vb index 714cbad..5a181c6 100644 --- a/SCrawler/API/Base/Declarations.vb +++ b/SCrawler/API/Base/Declarations.vb @@ -8,7 +8,11 @@ ' but WITHOUT ANY WARRANTY Namespace API.Base Friend Module Declarations + Friend Const UserLabelName As String = "User" Friend ReadOnly LNC As New ListAddParams(LAP.NotContainsOnly) + Friend ReadOnly UnixDate32Provider As New ADateTime(ADateTime.Formats.Unix32) + Friend ReadOnly UnixDate64Provider As New ADateTime(ADateTime.Formats.Unix64) + Friend ReadOnly HtmlConverter As Func(Of String, String) = Function(Input) SymbolsConverter.HTML.Decode(Input, EDP.ReturnValue) Friend ReadOnly TitleHtmlConverter As Func(Of String, String) = Function(Input) SymbolsConverter.HTML.Decode(SymbolsConverter.Convert(Input, EDP.ReturnValue), EDP.ReturnValue). StringRemoveWinForbiddenSymbols().StringTrim() diff --git a/SCrawler/API/Base/DownDetector.vb b/SCrawler/API/Base/DownDetector.vb index 72f8f14..e766a28 100644 --- a/SCrawler/API/Base/DownDetector.vb +++ b/SCrawler/API/Base/DownDetector.vb @@ -60,7 +60,7 @@ Namespace API.Base End Using Return l2 Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[DownDetector.GetData({Site})]") + Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, $"[DownDetector.GetData({Site})]") End Try End Function End Class diff --git a/SCrawler/API/Base/GDLBatch.vb b/SCrawler/API/Base/GDLBatch.vb new file mode 100644 index 0000000..7715b3c --- /dev/null +++ b/SCrawler/API/Base/GDLBatch.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.Tools +Imports PersonalUtilities.Functions.RegularExpressions +Namespace API.Base.GDL + Friend Module Declarations + Private Structure GDLURL : Implements IRegExCreator + Private _URL As String + Friend ReadOnly Property URL As String + Get + Return _URL + End Get + End Property + Public Shared Widening Operator CType(ByVal u As String) As GDLURL + Return New GDLURL With {._URL = u} + End Operator + Public Shared Widening Operator CType(ByVal u As GDLURL) As String + Return u.URL + End Operator + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists(2) Then + Dim u$ = ParamsArray(0).StringTrim.StringTrimEnd("/"), u2$ + If Not u.IsEmptyString Then + u2 = ParamsArray(1).StringTrim + If Not u2.IsEmptyString AndAlso u2.StartsWith("GET", StringComparison.OrdinalIgnoreCase) Then + u2 = u2.Remove(0, 3).StringTrim.StringTrimStart("/") + If Not u2.IsEmptyString Then _URL = $"{u}/{u2}" + End If + End If + End If + Return Me + End Function + Public Shared Operator =(ByVal x As GDLURL, ByVal y As GDLURL) As Boolean + Return x.URL = y.URL + End Operator + Public Shared Operator <>(ByVal x As GDLURL, ByVal y As GDLURL) As Boolean + Return Not x.URL = y.URL + End Operator + Public Overrides Function ToString() As String + Return URL + End Function + Public Overrides Function Equals(ByVal Obj As Object) As Boolean + Return URL = CType(Obj, String) + End Function + End Structure + Private ReadOnly Property GdlUrlPattern As RParams = RParams.DM(GDLBatch.UrlLibStart.Replace("[", "\[").Replace("]", "\]") & + "([^""]+?)""(GET [^""]+)""", 0, EDP.ReturnValue) + Friend Function GetUrlsFromGalleryDl(ByVal Batch As BatchExecutor, ByVal Command As String) As List(Of String) + Dim urls As New List(Of String) + Dim u As GDLURL + With Batch + .Execute(Command) + If .ErrorOutputData.Count > 0 Then + For Each eValue$ In .ErrorOutputData + u = RegexFields(Of GDLURL)(eValue, {GdlUrlPattern}, {1, 2}, EDP.ReturnValue).ListIfNothing.FirstOrDefault + If Not u.URL.IsEmptyString Then urls.ListAddValue(u, LNC) + Next + End If + End With + Return urls + End Function + End Module + Friend Class GDLBatch : Inherits BatchExecutor + Friend Property TempPostsList As List(Of String) + Friend Const UrlLibStart As String = "[urllib3.connectionpool][debug]" + Friend Const UrlTextStart As String = UrlLibStart & " https" + Friend Sub New() + MyBase.New(True) + ChangeDirectory(Settings.GalleryDLFile.File) + End Sub + Protected Overrides Async Sub OutputDataReceiver(ByVal Sender As Object, ByVal e As DataReceivedEventArgs) + MyBase.OutputDataReceiver(Sender, e) + Await Validate(e.Data) + End Sub + Protected Overridable Async Function Validate(ByVal Value As String) As Task + If Await Task.Run(Of Boolean)(Function() Not Value.IsEmptyString AndAlso + TempPostsList.Exists(Function(v) Value.Contains(v))) Then Kill(EDP.None) + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Base/M3U8Base.vb b/SCrawler/API/Base/M3U8Base.vb index f8b48f0..a712f4a 100644 --- a/SCrawler/API/Base/M3U8Base.vb +++ b/SCrawler/API/Base/M3U8Base.vb @@ -6,9 +6,12 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY -Imports PersonalUtilities.Functions.RegularExpressions +Imports System.Threading +Imports PersonalUtilities.Tools Imports PersonalUtilities.Tools.Web Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Forms.Toolbars +Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Base Namespace M3U8Declarations Friend Module M3U8Defaults @@ -16,6 +19,7 @@ Namespace API.Base End Module End Namespace Friend NotInheritable Class M3U8Base + Friend Const TempCacheFolderName As String = "tmpCache" Private Sub New() End Sub Friend Shared Function CreateUrl(ByVal Appender As String, ByVal File As String) As String @@ -28,36 +32,40 @@ Namespace API.Base 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 Responser = Nothing) As SFile - Dim CachePath As SFile = Nothing + Friend Shared Function Download(ByVal URLs As List(Of String), ByVal DestinationFile As SFile, Optional ByVal Responser As Responser = Nothing, + Optional ByVal Token As CancellationToken = Nothing, Optional ByVal Progress As MyProgress = Nothing) As SFile + Dim Cache As CacheKeeper = 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) + Cache = New CacheKeeper($"{DestinationFile.PathWithSeparator}_{TempCacheFolderName}\") + Dim cache2 As CacheKeeper = Cache.NewInstance + If cache2.RootDirectory.Exists(SFO.Path) Then + Dim progressExists As Boolean = Not Progress Is Nothing + If progressExists Then Progress.Maximum += URLs.Count + Dim p As SFileNumbers = SFileNumbers.Default(ConcatFile.Name) + ConcatFile = SFile.IndexReindex(ConcatFile,,, p, EDP.ReturnValue) Dim i% - Dim eFiles As New List(Of SFile) - Dim dFile As SFile = CachePath + Dim dFile As SFile = cache2.RootDirectory dFile.Extension = "ts" Using w As New DownloadObjects.WebClient2(Responser) For i = 0 To URLs.Count - 1 + If progressExists Then Progress.Perform() + Token.ThrowIfCancellationRequested() dFile.Name = $"ConPart_{i}" w.DownloadFile(URLs(i), dFile) - eFiles.Add(dFile) + cache2.AddFile(dFile, True) Next End Using - DestinationFile = FFMPEG.ConcatenateFiles(eFiles, Settings.FfmpegFile, ConcatFile, p, EDP.ThrowException) - eFiles.Clear() + DestinationFile = FFMPEG.ConcatenateFiles(cache2, Settings.FfmpegFile.File, ConcatFile, Settings.CMDEncoding, p, EDP.ThrowException) Return DestinationFile End If End If Return Nothing Finally - CachePath.Delete(SFO.Path, SFODelete.None, EDP.None) + Cache.DisposeIfReady End Try End Function End Class diff --git a/SCrawler/API/Base/ProfileSaved.vb b/SCrawler/API/Base/ProfileSaved.vb index 71e49b0..079a579 100644 --- a/SCrawler/API/Base/ProfileSaved.vb +++ b/SCrawler/API/Base/ProfileSaved.vb @@ -18,20 +18,17 @@ Namespace API.Base HOST = h Progress = Bar End Sub - Friend Sub Download(ByVal Token As CancellationToken) + Friend Sub Download(ByVal Token As CancellationToken, ByVal Multiple As Boolean) Try If HOST.Source.ReadyToDownload(PDownload.SavedPosts) Then - If HOST.Available(PDownload.SavedPosts, False) Then + If HOST.Available(PDownload.SavedPosts, Multiple) Then HOST.DownloadStarted(PDownload.SavedPosts) Dim u As New UserInfo With {.Plugin = HOST.Key, .Site = HOST.Name, .SpecialPath = HOST.SavedPostsPath} Using user As IUserData = HOST.GetInstance(PDownload.SavedPosts, Nothing, False, False) - If Not user Is Nothing AndAlso Not user.Name.IsEmptyString Then - u.Name = user.Name + If Not user Is Nothing Then With DirectCast(user, UserDataBase) - With .User : u.IsChannel = .IsChannel : u.UpdateUserFile() : End With - .User = u - .LoadUserInformation() .IsSavedPosts = True + .LoadUserInformation() .Progress = Progress If Not .FileExists Then .UpdateUserInformation() End With @@ -49,7 +46,7 @@ Namespace API.Base End If Catch ex As Exception Progress.InformationTemporary = $"{HOST.Name} downloading error" - ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[API.Base.ProfileSaved.Download({HOST.Key})]") + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"[API.Base.ProfileSaved.Download({HOST.Key})]") Finally HOST.DownloadDone(PDownload.SavedPosts) MainFrameObj.UpdateLogButton() diff --git a/SCrawler/API/Base/SiteSettingsBase.vb b/SCrawler/API/Base/SiteSettingsBase.vb index 6504616..d20b2a7 100644 --- a/SCrawler/API/Base/SiteSettingsBase.vb +++ b/SCrawler/API/Base/SiteSettingsBase.vb @@ -6,10 +6,9 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY -Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.Web.Clients -Imports PersonalUtilities.Tools.Web.Cookies Imports SCrawler.Plugin +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Functions.RegularExpressions Imports Download = SCrawler.Plugin.ISiteSettings.Download Namespace API.Base Friend MustInherit Class SiteSettingsBase : Implements ISiteSettings, IResponserContainer @@ -18,6 +17,23 @@ Namespace API.Base Friend Overridable ReadOnly Property Image As Image Implements ISiteSettings.Image Private Property Logger As ILogProvider = LogConnector Implements ISiteSettings.Logger Friend Overridable ReadOnly Property Responser As Responser + Friend ReadOnly Property CookiesNetscapeFile As SFile + Protected CheckNetscapeCookiesOnEndInit As Boolean = False + Private _UseNetscapeCookies As Boolean = False + Protected Property UseNetscapeCookies As Boolean + Get + Return _UseNetscapeCookies + End Get + Set(ByVal use As Boolean) + Dim b As Boolean = Not _UseNetscapeCookies = use + _UseNetscapeCookies = use + If Not Responser Is Nothing Then + Responser.Cookies.ChangedAllowInternalDrop = Not _UseNetscapeCookies + Responser.Cookies.Changed = False + End If + If b And _UseNetscapeCookies Then Update_SaveCookiesNetscape() + End Set + End Property Private Property IResponserContainer_Responser As Responser Implements IResponserContainer.Responser Get Return Responser @@ -27,20 +43,15 @@ Namespace API.Base Friend MustOverride Function GetInstance(ByVal What As Download) As IPluginContentProvider Implements ISiteSettings.GetInstance Friend Sub New(ByVal SiteName As String) Site = SiteName + CookiesNetscapeFile = $"{SettingsFolderName}\Responser_{Site}_Cookies_Netscape.txt" End Sub Friend Sub New(ByVal SiteName As String, ByVal CookiesDomain As String) - Site = SiteName - Responser = New Responser($"{SettingsFolderName}\Responser_{Site}.xml") + Me.New(SiteName) + Responser = New Responser($"{SettingsFolderName}\Responser_{Site}.xml") With {.DeclaredError = EDP.ThrowException} With Responser - If .File.Exists Then - If EncryptCookies.CookiesEncrypted Then .CookiesEncryptKey = SettingsCLS.CookieEncryptKey - .LoadSettings() - Else - .CookiesDomain = CookiesDomain - .CookiesEncryptKey = SettingsCLS.CookieEncryptKey - .SaveSettings() - End If - If .CookiesDomain.IsEmptyString Then .CookiesDomain = CookiesDomain + .CookiesDomain = CookiesDomain + .CookiesEncryptKey = SettingsCLS.CookieEncryptKey + If .File.Exists Then .LoadSettings() Else .SaveSettings() End With End Sub #Region "XML" @@ -51,17 +62,47 @@ Namespace API.Base Friend Overridable Sub BeginInit() Implements ISiteSettings.BeginInit End Sub Friend Overridable Sub EndInit() Implements ISiteSettings.EndInit - EncryptCookies.ValidateCookiesEncrypt(Responser) If Not DefaultUserAgent.IsEmptyString And Not Responser Is Nothing Then Responser.UserAgent = DefaultUserAgent + If CheckNetscapeCookiesOnEndInit Then Update_SaveCookiesNetscape(, True) End Sub +#End Region +#Region "Update, Edit" Friend Overridable Sub BeginUpdate() Implements ISiteSettings.BeginUpdate End Sub Friend Overridable Sub EndUpdate() Implements ISiteSettings.EndUpdate End Sub + Protected _SiteEditorFormOpened As Boolean = False Friend Overridable Sub BeginEdit() Implements ISiteSettings.BeginEdit + _SiteEditorFormOpened = True End Sub Friend Overridable Sub EndEdit() Implements ISiteSettings.EndEdit + If _SiteEditorFormOpened Then DomainsReset() + _SiteEditorFormOpened = False End Sub + Friend Overridable Sub Update() Implements ISiteSettings.Update + If _SiteEditorFormOpened Then + If UseNetscapeCookies Then Update_SaveCookiesNetscape() + DomainsApply() + End If + If Not Responser Is Nothing Then Responser.SaveSettings() + End Sub + Protected Sub Update_SaveCookiesNetscape(Optional ByVal Force As Boolean = False, Optional ByVal IsInit As Boolean = False) + If Not Responser Is Nothing Then + With Responser + If .Cookies.Changed Or Force Or IsInit Then + If IsInit And CookiesNetscapeFile.Exists Then Exit Sub + If .CookiesExists Then .Cookies.SaveNetscapeFile(CookiesNetscapeFile) Else CookiesNetscapeFile.Delete() + .Cookies.Changed = False + End If + End With + End If + End Sub +#Region "Specialized" + Protected Overridable Sub DomainsApply() + End Sub + Protected Overridable Sub DomainsReset() + End Sub +#End Region #End Region #Region "Before and After Download" Friend Overridable Sub DownloadStarted(ByVal What As Download) Implements ISiteSettings.DownloadStarted @@ -75,20 +116,15 @@ Namespace API.Base #End Region #Region "User info" Protected UrlPatternUser As String = String.Empty - Protected UrlPatternChannel As String = String.Empty - 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, User.Name) - Else - If Not UrlPatternUser.IsEmptyString Then Return String.Format(UrlPatternUser, User.Name) - End If + Friend Overridable Function GetUserUrl(ByVal User As IPluginContentProvider) As String Implements ISiteSettings.GetUserUrl + If Not UrlPatternUser.IsEmptyString Then Return String.Format(UrlPatternUser, User.Name) Return String.Empty End Function 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 + Return Media.URL_BASE.IfNullOrEmpty(Media.URL) End Function Protected UserRegex As RParams = Nothing Friend Overridable Function IsMyUser(ByVal UserURL As String) As ExchangeOptions Implements ISiteSettings.IsMyUser @@ -99,43 +135,40 @@ Namespace API.Base End If Return Nothing Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[API.Base.SiteSettingsBase.IsMyUser({UserURL})]", New ExchangeOptions) + Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, $"[API.Base.SiteSettingsBase.IsMyUser({UserURL})]", New ExchangeOptions) End Try End Function Protected ImageVideoContains As String = String.Empty Friend Overridable Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions Implements ISiteSettings.IsMyImageVideo If Not ImageVideoContains.IsEmptyString AndAlso URL.Contains(ImageVideoContains) Then - Return New ExchangeOptions With {.Exists = True} + Return New ExchangeOptions(Site, String.Empty) With {.Exists = True} Else Return Nothing End If End Function - Friend Overridable Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable Implements ISiteSettings.GetSpecialData - Return Nothing + Private Function ISiteSettings_GetSingleMediaInstance(ByVal URL As String, ByVal OutputFile As String) As IDownloadableMedia Implements ISiteSettings.GetSingleMediaInstance + Return GetSingleMediaInstance(URL, OutputFile) End Function - Friend Shared Function GetSpecialDataFile(ByVal Path As String, ByVal AskForPath As Boolean, ByRef SpecFolderObj As String) As SFile - Dim f As SFile = Path.CSFileP - If f.Name.IsEmptyString Then f.Name = "OutputFile" -#Disable Warning BC40000 - If Path.CSFileP.IsEmptyString Or AskForPath Then f = SFile.SaveAs(f, "File destination",,,, EDP.ReturnValue) : SpecFolderObj = f.Path -#Enable Warning - Return f + Friend Overridable Function GetSingleMediaInstance(ByVal URL As String, ByVal OutputFile As SFile) As IDownloadableMedia + Return New Hosts.DownloadableMediaHost(URL, OutputFile) End Function #End Region #Region "Ready, Available" + ''' True Friend Overridable Function BaseAuthExists() As Boolean Return True End Function + ''' JOB: leave or remove + ''' Return BaseAuthExists() Friend Overridable Function Available(ByVal What As Download, ByVal Silent As Boolean) As Boolean Implements ISiteSettings.Available Return BaseAuthExists() End Function + ''' 'DownloadData': before processing + ''' True Friend Overridable Function ReadyToDownload(ByVal What As Download) As Boolean Implements ISiteSettings.ReadyToDownload Return True End Function #End Region - Friend Overridable Sub Update() Implements ISiteSettings.Update - If Not Responser Is Nothing Then Responser.SaveSettings() - End Sub Friend Overridable Sub Reset() Implements ISiteSettings.Reset End Sub Friend Overridable Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) Implements ISiteSettings.UserOptions diff --git a/SCrawler/API/Base/Structures.vb b/SCrawler/API/Base/Structures.vb index df56a67..eaf82a4 100644 --- a/SCrawler/API/Base/Structures.vb +++ b/SCrawler/API/Base/Structures.vb @@ -27,10 +27,12 @@ Namespace API.Base #End Region Friend Enum Types As Integer Undefined = 0 - [Picture] = 1 - [Video] = 2 - [Text] = 3 + Picture = 1 + Video = 2 + Audio = 200 + Text = 4 VideoPre = 10 + AudioPre = 215 GIF = 50 m3u8 = 100 End Enum @@ -51,12 +53,12 @@ Namespace API.Base Friend SpecialFolder As String Friend [Object] As Object #Region "Interface Support" - Private Property IUserMedia_Type As Integer Implements IUserMedia.ContentType + Private Property IUserMedia_Type As UserMediaTypes Implements IUserMedia.ContentType Get - Return Type + Return CInt(Type) End Get - Set(ByVal Type As Integer) - Me.Type = Type + Set(ByVal Type As UserMediaTypes) + Me.Type = CInt(Type) End Set End Property Private Property IUserMedia_URL_BASE As String Implements IUserMedia.URL_BASE @@ -91,12 +93,12 @@ Namespace API.Base Me.File = File End Set End Property - Private Property IUserMedia_State As Integer Implements IUserMedia.DownloadState + Private Property IUserMedia_State As UserMediaStates Implements IUserMedia.DownloadState Get - Return State + Return CInt(State) End Get - Set(ByVal State As Integer) - Me.State = State + Set(ByVal State As UserMediaStates) + Me.State = CInt(State) End Set End Property Private Property IUserMedia_PostID As String Implements IUserMedia.PostID diff --git a/SCrawler/API/Base/UserDataBase.vb b/SCrawler/API/Base/UserDataBase.vb index 2358142..e6e3c85 100644 --- a/SCrawler/API/Base/UserDataBase.vb +++ b/SCrawler/API/Base/UserDataBase.vb @@ -9,6 +9,7 @@ Imports System.IO Imports System.Net Imports System.Threading +Imports System.ComponentModel Imports System.Runtime.CompilerServices Imports SCrawler.Plugin Imports SCrawler.Plugin.Hosts @@ -18,6 +19,7 @@ Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Forms.Toolbars Imports PersonalUtilities.Tools Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.ImageRenderer Imports UStates = SCrawler.API.Base.UserMedia.States Imports UTypes = SCrawler.API.Base.UserMedia.Types Namespace API.Base @@ -98,7 +100,7 @@ Namespace API.Base #Region "XML Declarations" 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 + Protected Const Name_IsChannel As String = "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 @@ -108,9 +110,9 @@ Namespace API.Base Private Const Name_UserExists As String = "UserExists" Private Const Name_UserSuspended As String = "UserSuspended" - Private Const Name_FriendlyName As String = "FriendlyName" + Protected Const Name_FriendlyName As String = "FriendlyName" Private Const Name_UserSiteName As String = "UserSiteName" - Private Const Name_UserID As String = "UserID" + Protected Const Name_UserID As String = "UserID" Private Const Name_Description As String = "Description" Private Const Name_ParseUserMediaOnly As String = "ParseUserMediaOnly" Private Const Name_Temporary As String = "Temporary" @@ -132,10 +134,12 @@ Namespace API.Base Private Const Name_ScriptUse As String = "ScriptUse" Private Const Name_ScriptData As String = "ScriptData" - Friend Const Name_DataMerging As String = "DataMerging" + Protected Const Name_UseMD5Comparison As String = "UseMD5Comparison" + Protected Const Name_RemoveExistingDuplicates As String = "RemoveExistingDuplicates" + Protected Const Name_StartMD5Checked As String = "StartMD5Checked" #End Region #Region "Declarations" -#Region "Host, Site, Progress, Self" +#Region "Host, Site, Progress" Friend Property HOST As SettingsHost Implements IUserData.HOST Friend ReadOnly Property Site As String Implements IContentProvider.Site Get @@ -167,15 +171,17 @@ Namespace API.Base Me._UserSuspended = _UserSuspended End Set End Property - Friend Overridable Property Name As String Implements IContentProvider.Name, IPluginContentProvider.Name + Private Property IPluginContentProvider_Name As String Implements IPluginContentProvider.Name + Get + Return Name + End Get + Set(ByVal NewName As String) + End Set + End Property + Friend Overridable ReadOnly Property Name As String Implements IContentProvider.Name Get Return User.Name End Get - Set(ByVal NewName As String) - User.Name = NewName - User.UpdateUserFile() - Settings.UpdateUsersList(User) - End Set End Property Friend Overridable Property ID As String = String.Empty Implements IContentProvider.ID, IPluginContentProvider.ID Protected _FriendlyName As String = String.Empty @@ -275,11 +281,6 @@ Namespace API.Base End Property #End Region #Region "Channel" - Friend Overridable ReadOnly Property IsChannel As Boolean Implements IUserData.IsChannel - Get - Return User.IsChannel - End Get - End Property Friend Property CreatedByChannel As Boolean = False #End Region #Region "Images" @@ -564,7 +565,7 @@ BlockNullPicture: #End Region #Region "Plugins Support" Protected Event ProgressChanged As IPluginContentProvider.ProgressChangedEventHandler Implements IPluginContentProvider.ProgressChanged - Protected Event TotalCountChanged As IPluginContentProvider.TotalCountChangedEventHandler Implements IPluginContentProvider.TotalCountChanged + Protected Event ProgressMaximumChanged As IPluginContentProvider.ProgressMaximumChangedEventHandler Implements IPluginContentProvider.ProgressMaximumChanged Private Property IPluginContentProvider_Settings As ISiteSettings Implements IPluginContentProvider.Settings Get Return HOST.Source @@ -585,9 +586,11 @@ BlockNullPicture: Private Function IPluginContentProvider_XmlFieldsGet() As List(Of KeyValuePair(Of String, String)) Implements IPluginContentProvider.XmlFieldsGet Return Nothing End Function - Private Sub IPluginContentProvider_GetMedia() Implements IPluginContentProvider.GetMedia + Private Sub IPluginContentProvider_GetMedia(ByVal Token As CancellationToken) Implements IPluginContentProvider.GetMedia End Sub - Private Sub IPluginContentProvider_Download() Implements IPluginContentProvider.Download + Private Sub IPluginContentProvider_Download(ByVal Token As CancellationToken) Implements IPluginContentProvider.Download + End Sub + Private Sub IPluginContentProvider_DownloadSingleObject(ByVal Data As IDownloadableMedia, ByVal Token As CancellationToken) Implements IPluginContentProvider.DownloadSingleObject End Sub Friend Overridable Function ExchangeOptionsGet() As Object Implements IPluginContentProvider.ExchangeOptionsGet Return Nothing @@ -598,8 +601,8 @@ BlockNullPicture: #End Region #Region "IIndexable Support" Friend Property Index As Integer = 0 Implements IIndexable.Index - Private Function SetIndex(ByVal Obj As Object, ByVal _Index As Integer) As Object Implements IIndexable.SetIndex - DirectCast(Obj, UserDataBase).Index = _Index + Private Function SetIndex(ByVal Obj As Object, ByVal Index As Integer) As Object Implements IIndexable.SetIndex + DirectCast(Obj, UserDataBase).Index = Index Return Obj End Function #End Region @@ -607,7 +610,7 @@ BlockNullPicture: Friend ReadOnly Property LVIKey As String Implements IUserData.Key Get If Not _IsCollection Then - Return $"{IIf(IsChannel, "C", String.Empty)}{Site.ToString.ToUpper}_{Name}" + Return $"{Site.ToString.ToUpper}_{Name}" Else Return $"CCCC_{CollectionName}" End If @@ -658,7 +661,7 @@ BlockNullPicture: Next End If ElseIf Settings.ShowGroups Then - Return Destination.Groups.Item(GetLviGroupName(HOST, Temporary, Favorite, IsCollection, IsChannel)) + Return Destination.Groups.Item(GetLviGroupName(HOST, Temporary, Favorite, IsCollection)) End If Return Destination.Groups.Item(LabelsKeeper.NoLabeledName) Catch ex As Exception @@ -689,7 +692,7 @@ BlockNullPicture: ''' Friend Shared Function GetInstance(ByVal u As UserInfo, Optional ByVal _LoadUserInformation As Boolean = True) As IUserData If Not u.Plugin.IsEmptyString Then - Return Settings(u.Plugin).GetInstance(u.DownloadOption, u, _LoadUserInformation) + Return Settings(u.Plugin).GetInstance(ISiteSettings.Download.Main, u, _LoadUserInformation) Else Throw New ArgumentOutOfRangeException("Plugin", $"Plugin [{u.Plugin}] information does not recognized by loader") End If @@ -707,7 +710,7 @@ BlockNullPicture: End If Return String.Empty Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"GetPostUrl({uName}, {PostData.Post.ID})", String.Empty) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, $"GetPostUrl({uName}, {PostData.Post.ID})", String.Empty) End Try End Function #End Region @@ -716,7 +719,7 @@ BlockNullPicture: Private _UserInformationLoaded As Boolean = False Friend Overridable Sub LoadUserInformation() Implements IUserData.LoadUserInformation Try - UpdateDataFiles(, True) + UpdateDataFiles() If MyFileSettings.Exists Then FileExists = True Using x As New XmlFile(MyFileSettings) With {.XmlReadOnly = True} @@ -740,14 +743,7 @@ 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) - 'TODELETE: UserDataBase remove old 'merge' constant -#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 + DataMerging = x.Value(Name_Merged).FromXML(Of Boolean)(False) 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) @@ -762,13 +758,12 @@ BlockNullPicture: End Sub Friend Overridable Sub UpdateUserInformation() Implements IUserData.UpdateUserInformation Try - UpdateDataFiles(True) + UpdateDataFiles() MyFileSettings.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) @@ -815,7 +810,7 @@ BlockNullPicture: #Region "User data" Friend Overridable Overloads Sub LoadContentInformation(Optional ByVal Force As Boolean = False) Try - UpdateDataFiles(, True) + UpdateDataFiles() If Not MyFileData.Exists Or (_DataLoaded And Not Force) Then Exit Sub Using x As New XmlFile(MyFileData, Protector.Modes.All, False) With {.XmlReadOnly = True, .AllowSameNames = True} x.LoadData() @@ -830,7 +825,7 @@ BlockNullPicture: End Sub Friend Sub UpdateContentInformation() Try - UpdateDataFiles(True, True) + UpdateDataFiles() If MyFileData.IsEmptyString Then Exit Sub MyFileData.Exists(SFO.Path) Using x As New XmlFile With {.AllowSameNames = True, .Name = "Data"} @@ -846,7 +841,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(Me, IsChannel) + Dim URL$ = HOST.Source.GetUserUrl(Me) If Not URL.IsEmptyString Then Process.Start(URL) Catch ex As Exception If Not e.Exists Then e = New ErrorsDescriber(EDP.ShowAllMsg) @@ -886,7 +881,7 @@ BlockNullPicture: Protected Function CheckDatesLimit(ByVal DateObj As Object, ByVal DateProvider As IFormatProvider) As DateResult Try If (DownloadDateFrom.HasValue Or DownloadDateTo.HasValue) AndAlso ACheck(DateObj) Then - Dim td As Date? = AConvert(Of Date)(DateObj, DateProvider, Nothing) + Dim td As Date? = AConvert(DateObj, AModes.Var, GetType(Date),, True, Nothing, DateProvider) If td.HasValue Then If td.Value.ValueBetween(_DownloadDateFromF, _DownloadDateToF) Then Return DateResult.Continue @@ -899,13 +894,14 @@ BlockNullPicture: End If Return DateResult.Continue Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[UserDataBase.CheckDatesLimit({If(TypeOf DateObj Is String, CStr(DateObj), "?")})]", DateResult.Continue) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, $"[UserDataBase.CheckDatesLimit({If(TypeOf DateObj Is String, CStr(DateObj), "?")})]", DateResult.Continue) End Try End Function #End Region #Region "Download functions and options" Protected Responser As Responser Protected UseResponserClient As Boolean = False + Protected UseClientTokens As Boolean = False Protected _ForceSaveUserData As Boolean = False Protected _ForceSaveUserInfo As Boolean = False Private _DownloadInProgress As Boolean = False @@ -915,7 +911,7 @@ BlockNullPicture: Private _PictureExists As Boolean Private _EnvirInvokeUserUpdated As Boolean = False Protected Sub EnvirDownloadSet() - UpdateDataFiles(, True) + UpdateDataFiles() _DownloadInProgress = True _DescriptionChecked = False _DescriptionEveryTime = Settings.UpdateUserDescriptionEveryTime @@ -948,8 +944,8 @@ BlockNullPicture: If Not Responser Is Nothing Then Responser.Dispose() Responser = New Responser If Not HOST.Responser Is Nothing Then Responser.Copy(HOST.Responser) - 'TODO: UserDataBase remove [Responser.DecodersError] - Responser.DecodersError = New ErrorsDescriber(EDP.SendInLog + EDP.ReturnValue) With { + + Responser.DecodersError = New ErrorsDescriber(EDP.SendToLog + EDP.ReturnValue) With { .DeclaredMessage = New MMessage($"SymbolsConverter error: [{ToStringForLog()}]", ToStringForLog())} Dim _downContent As Func(Of UserMedia, Boolean) = Function(c) c.State = UStates.Downloaded @@ -981,7 +977,9 @@ BlockNullPicture: ReparseVideo(Token) ThrowAny(Token) - If IsSavedPosts Then UpdateDataFiles(True) + + If UseMD5Comparison Then ValidateMD5(Token) : ThrowAny(Token) + If _TempPostsList.Count > 0 And Not DownloadMissingOnly And __SaveData Then _ TextSaver.SaveTextToFile(_TempPostsList.ListToString(Environment.NewLine), MyFilePosts, True,, EDP.None) _ContentNew.ListAddList(_TempMediaList, LAP.ClearBeforeAdd) @@ -1035,19 +1033,14 @@ BlockNullPicture: _ForceSaveUserInfo = False End Try End Sub - Protected Sub UpdateDataFiles(Optional ByVal ForceSaved As Boolean = False, Optional ByVal ValidateContetnt As Boolean = False) - 'TODELETE: saved posts name compatibility 2023.2.5.0 - Dim __validateSaved As Func(Of Boolean) = Function() MyFileData.Exists Or MyFilePosts.Exists - If Not User.File.IsEmptyString Then + Protected Sub UpdateDataFiles() + If Not User.File.IsEmptyString OrElse IsSavedPosts Then MyFileSettings = Nothing If IsSavedPosts Then - Dim u As UserInfo = User - u.Name = "SavedPosts" - u.UpdateUserFile() - Dim mfp As SFile = u.File - mfp.Name &= "_Posts" - mfp.Extension = "txt" - If (ValidateContetnt AndAlso mfp.Exists) Or (Not ValidateContetnt AndAlso u.File.Exists) Or ForceSaved Then MyFileSettings = u.File + User = New UserInfo(SettingsHost.SavedPostsFolderName, HOST) + User.File.Path = $"{HOST.SavedPostsPath.PathWithSeparator}{SettingsFolderName}" + MyFileSettings = User.File + MyFileSettings.Name = MyFileSettings.Name.Replace(SettingsHost.SavedPostsFolderName, "SavedPosts") End If If MyFileSettings.IsEmptyString Then MyFileSettings = User.File MyFileData = MyFileSettings @@ -1060,6 +1053,71 @@ BlockNullPicture: End If End Sub Protected MustOverride Sub DownloadDataF(ByVal Token As CancellationToken) +#Region "DownloadSingleObject" + Protected IsSingleObjectDownload As Boolean = False + Friend Overridable Sub DownloadSingleObject(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal Token As CancellationToken) Implements IUserData.DownloadSingleObject + Try + Data.DownloadState = UserMediaStates.Tried + Progress = Data.Progress + If Not Responser Is Nothing Then Responser.Dispose() + Responser = New Responser + If Not HOST Is Nothing AndAlso Not HOST.Responser Is Nothing Then Responser.Copy(HOST.Responser) + SeparateVideoFolder = False + IsSingleObjectDownload = True + UseInternalDownloadFileFunction_UseProgress = True + UseInternalM3U8Function_UseProgress = True + DownloadSingleObject_GetPosts(Data, Token) + DownloadSingleObject_CreateMedia(Data, Token) + DownloadSingleObject_Download(Data, Token) + DownloadSingleObject_PostProcessing(Data) + Catch ex As Exception + Data.DownloadState = UserMediaStates.Missing + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"{Site} single data downloader error: {Data.URL}") + End Try + End Sub + Protected Overridable Sub DownloadSingleObject_CreateMedia(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal Token As CancellationToken) + If _TempMediaList.Count > 0 Then + For Each m As UserMedia In _TempMediaList + m.File = DownloadSingleObject_CreateFile(Data, m.File) + _ContentNew.Add(m) + Next + End If + End Sub + Protected Overridable Sub DownloadSingleObject_PostProcessing(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, Optional ByVal ResetTitle As Boolean = True) + If _ContentNew.Count > 0 Then + If _ContentNew.Any(Function(mm) mm.State = UStates.Downloaded) Then + Data.DownloadState = UserMediaStates.Downloaded + If _ContentNew(0).Type = UTypes.Picture Or _ContentNew(0).Type = UTypes.GIF Then + DirectCast(Data, IDownloadableMedia).ThumbnailFile = _ContentNew(0).File + ElseIf Settings.STDownloader_TakeSnapshot And Settings.FfmpegFile.Exists And Not Settings.STDownloader_RemoveDownloadedAutomatically Then + Dim f As SFile = _ContentNew(0).File + Dim ff As SFile = f + ff.Name &= "_thumb" + ff.Extension = "jpg" + f = Web.FFMPEG.TakeSnapshot(f, ff, Settings.FfmpegFile, TimeSpan.FromSeconds(1),,, EDP.LogMessageValue) + If f.Exists Then DirectCast(Data, IDownloadableMedia).ThumbnailFile = f + End If + Else + Data.DownloadState = UserMediaStates.Missing + End If + YouTube.Objects.YouTubeMediaContainerBase.Update(_ContentNew(0), Data) + If ResetTitle And Not _ContentNew(0).File.Name.IsEmptyString Then Data.Title = _ContentNew(0).File.Name + Else + Data.DownloadState = UserMediaStates.Missing + End If + End Sub + Protected Function DownloadSingleObject_CreateFile(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal DFile As SFile) As SFile + If Not Data.File.Path.IsEmptyString Then DFile.Path = Data.File.Path + If DFile.Name.IsEmptyString Then DFile.Name = "OutputFile" + Return DFile + End Function + Protected Overridable Sub DownloadSingleObject_Download(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal Token As CancellationToken) + DownloadContent(Token) + End Sub + Protected Overridable Sub DownloadSingleObject_GetPosts(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal Token As CancellationToken) + End Sub +#End Region +#Region "ReparseVideo, ReparseMissing" Protected Overridable Sub ReparseVideo(ByVal Token As CancellationToken) End Sub ''' @@ -1069,15 +1127,170 @@ BlockNullPicture: ''' Protected Overridable Sub ReparseMissing(ByVal Token As CancellationToken) End Sub +#End Region +#Region "MD5 support" + Protected Const VALIDATE_MD5_ERROR As String = "VALIDATE_MD5_ERROR" + Friend Property UseMD5Comparison As Boolean = False + Protected Property StartMD5Checked As Boolean = True + Friend Property RemoveExistingDuplicates As Boolean = False + Protected Overridable Sub ValidateMD5(ByVal Token As CancellationToken) + Try + Dim missingMD5 As Predicate(Of UserMedia) = Function(d) (d.Type = UTypes.GIF Or d.Type = UTypes.Picture) And d.MD5.IsEmptyString + If UseMD5Comparison And _TempMediaList.Exists(missingMD5) Then + Dim i% + Dim itemsCount% = 0 + Dim limit% = If(DownloadTopCount, 0) + Dim data As UserMedia = Nothing + Dim hashList As New Dictionary(Of String, SFile) + Dim f As SFile + Dim ErrMD5 As New ErrorsDescriber(EDP.ReturnValue) + Dim __getMD5 As Func(Of UserMedia, Boolean, String) = + Function(ByVal __data As UserMedia, ByVal IsUrl As Boolean) As String + Try + Dim ImgFormat As Imaging.ImageFormat = Nothing + Dim hash$ = String.Empty + Dim __isGif As Boolean = False + If __data.Type = UTypes.GIF Then + ImgFormat = Imaging.ImageFormat.Gif + __isGif = True + ElseIf Not __data.File.IsEmptyString Then + ImgFormat = GetImageFormat(__data.File) + End If + If ImgFormat Is Nothing Then ImgFormat = Imaging.ImageFormat.Jpeg + If IsUrl Then + hash = ByteArrayToString(GetMD5(SFile.GetBytesFromNet(__data.URL_BASE.IfNullOrEmpty(__data.URL), ErrMD5), ImgFormat, ErrMD5)) + Else + hash = ByteArrayToString(GetMD5(SFile.GetBytes(__data.File, ErrMD5), ImgFormat, ErrMD5)) + End If + If hash.IsEmptyString And Not __isGif Then + If ImgFormat Is Imaging.ImageFormat.Jpeg Then ImgFormat = Imaging.ImageFormat.Png Else ImgFormat = Imaging.ImageFormat.Jpeg + If IsUrl Then + hash = ByteArrayToString(GetMD5(SFile.GetBytesFromNet(__data.URL_BASE.IfNullOrEmpty(__data.URL), ErrMD5), ImgFormat, ErrMD5)) + Else + hash = ByteArrayToString(GetMD5(SFile.GetBytes(__data.File, ErrMD5), ImgFormat, ErrMD5)) + End If + End If + Return hash + Catch + Return String.Empty + End Try + End Function + If Not StartMD5Checked Then + StartMD5Checked = True + If _ContentList.Exists(missingMD5) Then + Dim existingFiles As List(Of SFile) = SFile.GetFiles(MyFileSettings.CutPath, "*.jpg|*.jpeg|*.png|*.gif",, EDP.ReturnValue).ListIfNothing + Dim eIndx% + Dim eFinder As Predicate(Of SFile) = Function(ff) ff.File = data.File.File + If RemoveExistingDuplicates Then + RemoveExistingDuplicates = False + _ForceSaveUserInfo = True + If existingFiles.Count > 0 Then + Dim h$ + For i = existingFiles.Count - 1 To 0 Step -1 + h = __getMD5(New UserMedia With {.File = existingFiles(i)}, False) + If Not h.IsEmptyString Then + If hashList.ContainsKey(h) Then + MyMainLOG = $"{ToStringForLog()}: Removed image [{existingFiles(i).File}] (duplicate of [{hashList(h).File}])" + existingFiles(i).Delete(SFO.File, SFODelete.DeleteToRecycleBin, ErrMD5) + existingFiles.RemoveAt(i) + Else + hashList.Add(h, existingFiles(i)) + End If + End If + Next + End If + End If + For i = 0 To _ContentList.Count - 1 + data = _ContentList(i) + If (data.Type = UTypes.GIF Or data.Type = UTypes.Picture) Then + If data.MD5.IsEmptyString Then + ThrowAny(Token) + eIndx = existingFiles.FindIndex(eFinder) + If eIndx >= 0 Then + data.MD5 = __getMD5(New UserMedia With {.File = existingFiles(eIndx)}, False) + If Not data.MD5.IsEmptyString Then _ContentList(i) = data : _ForceSaveUserData = True + End If + End If + existingFiles.RemoveAll(eFinder) + End If + Next + If existingFiles.Count > 0 Then + For i = 0 To existingFiles.Count - 1 + f = existingFiles(i) + data = New UserMedia(f.File) With { + .State = UStates.Downloaded, + .Type = IIf(f.Extension = "gif", UTypes.GIF, UTypes.Picture), + .File = f + } + ThrowAny(Token) + data.MD5 = __getMD5(data, False) + If Not data.MD5.IsEmptyString Then _ContentList.Add(data) : _ForceSaveUserData = True + Next + existingFiles.Clear() + End If + End If + End If + + If _ContentList.Count > 0 Then + With _ContentList.Select(Function(d) d.MD5) + If .ListExists Then .ToList.ForEach(Sub(md5value) _ + If Not md5value.IsEmptyString AndAlso Not hashList.ContainsKey(md5value) Then hashList.Add(md5value, New SFile)) + End With + End If + + For i = _TempMediaList.Count - 1 To 0 Step -1 + If limit > 0 And itemsCount >= limit Then + _TempMediaList.RemoveAt(i) + Else + data = _TempMediaList(i) + If missingMD5(data) Then + ThrowAny(Token) + data.MD5 = __getMD5(data, True) + If Not data.MD5.IsEmptyString Then + If hashList.ContainsKey(data.MD5) Then + _TempMediaList.RemoveAt(i) + Else + hashList.Add(data.MD5, New SFile) + _TempMediaList(i) = data + itemsCount += 1 + End If + End If + End If + End If + Next + End If + Catch iex As ArgumentOutOfRangeException When Disposed + Catch ex As Exception + ProcessException(ex, Token, "ValidateMD5",, VALIDATE_MD5_ERROR) + End Try + End Sub +#End Region +#Region "DownloadContent" Protected MustOverride Sub DownloadContent(ByVal Token As CancellationToken) Private NotInheritable Class OptionalWebClient : Inherits DownloadObjects.WebClient2 + Private ReadOnly Source As UserDataBase Friend Sub New(ByRef Source As UserDataBase) + Me.Source = Source UseResponserClient = Source.UseResponserClient If UseResponserClient Then - RC = Source.Responser + Client = Source.Responser Else - WC = New WebClient + Client = New RWebClient With {.UseNativeClient = Not Source.IsSingleObjectDownload} End If + If Source.IsSingleObjectDownload Then DelegateEvents = True + End Sub + Private _LastProgressValue As Integer = 0 + Protected Overrides Sub Client_DownloadProgressChanged(ByVal Sender As Object, ByVal e As DownloadProgressChangedEventArgs) + Dim v% = e.ProgressPercentage + If v > _LastProgressValue Then + If v > 100 Then v = 100 + Source.Progress.Value = v + Source.Progress.Perform(0) + End If + _LastProgressValue = e.ProgressPercentage + End Sub + Protected Overrides Sub Client_DownloadFileCompleted(ByVal Sender As Object, ByVal e As AsyncCompletedEventArgs) + Source.Progress.Done() End Sub End Class Protected Sub DownloadContentDefault(ByVal Token As CancellationToken) @@ -1090,37 +1303,51 @@ BlockNullPicture: If _ContentNew.Count > 0 Then MyFile.Exists(SFO.Path) Dim MissingErrorsAdd As Boolean = Settings.AddMissingErrorsToLog - Dim MyDir$ = MyFile.CutPath.PathNoSeparator + Dim MyDir$ = DownloadContentDefault_GetRootDir() Dim vsf As Boolean = SeparateVideoFolderF Dim __isVideo As Boolean + Dim __interrupt As Boolean Dim f As SFile Dim v As UserMedia + Dim fileNumProvider As SFileNumbers = SFileNumbers.Default Using w As New OptionalWebClient(Me) If vsf Then CSFileP($"{MyDir}\Video\").Exists(SFO.Path) Progress.Maximum += _ContentNew.Count + If IsSingleObjectDownload Then + If _ContentNew.Count = 1 And _ContentNew(0).Type = UTypes.Video Then + Progress.Value = 0 + Progress.Maximum = 100 + Progress.Provider = MyProgressNumberProvider.Percentage + ElseIf _ContentNew(0).Type = UTypes.m3u8 Then + Progress.Provider = MyProgressNumberProvider.Percentage + Else + w.DelegateEvents = False + End If + End If + For i = 0 To _ContentNew.Count - 1 ThrowAny(Token) v = _ContentNew(i) v.State = UStates.Tried If v.File.IsEmptyString Then - f = v.URL + f = CreateFileFromUrl(v.URL) Else f = v.File End If f.Separator = "\" - f.Path = MyDir + If Not IsSingleObjectDownload Then f.Path = MyDir If v.URL_BASE.IsEmptyString Then v.URL_BASE = v.URL - If Not v.File.IsEmptyString And Not v.URL.IsEmptyString Then + If Not f.IsEmptyString And Not v.URL.IsEmptyString Then Try - __isVideo = v.Type = UTypes.Video Or f.Extension = "mp4" + __isVideo = v.Type = UTypes.Video Or f.Extension = "mp4" Or v.Type = UTypes.m3u8 If f.Extension.IsEmptyString Then Select Case v.Type Case UTypes.Picture : f.Extension = "jpg" - Case UTypes.Video : f.Extension = "mp4" + Case UTypes.Video, UTypes.m3u8 : f.Extension = "mp4" Case UTypes.GIF : f.Extension = "gif" End Select ElseIf f.Extension = "webp" And Settings.DownloadNativeImageFormat Then @@ -1138,16 +1365,29 @@ BlockNullPicture: End If End If + If __isVideo Then fileNumProvider.FileName = f.Name : f = SFile.IndexReindex(f,,, fileNumProvider) + + __interrupt = False If v.Type = UTypes.m3u8 And UseInternalM3U8Function Then - f = DownloadM3U8(v.URL, v, f) + f = DownloadM3U8(v.URL, v, f, Token) If f.IsEmptyString Then Throw New Exception("M3U8 download failed") + ElseIf UseInternalDownloadFileFunction AndAlso ValidateDownloadFile(v.URL, v, __interrupt) Then + f = DownloadFile(v.URL, v, f, Token) + If f.IsEmptyString Then Throw New Exception("InternalFunc download failed") Else - w.DownloadFile(v.URL, f.ToString) + If UseInternalDownloadFileFunction And __interrupt Then Throw New Exception("InternalFunc download interrupted") + If UseClientTokens Then + w.DownloadFile(v.URL, f, Token) + Else + w.DownloadFile(v.URL, f) + End If End If If __isVideo Then v.Type = UTypes.Video DownloadedVideos(False) += 1 + ElseIf v.Type = UTypes.GIF Then + DownloadedPictures(False) += 1 Else v.Type = UTypes.Picture DownloadedPictures(False) += 1 @@ -1155,11 +1395,20 @@ BlockNullPicture: v.File = ChangeFileNameByProvider(f, v) v.State = UStates.Downloaded + DownloadContentDefault_PostProcessing(v, f, Token) dCount += 1 - Catch wex As Exception - v.Attempts += 1 + Catch woex As OperationCanceledException When Token.IsCancellationRequested + If f.Exists Then f.Delete() v.State = UStates.Missing - If MissingErrorsAdd Then ErrorDownloading(f, v.URL) + v.Attempts += 1 + _ContentNew(i) = v + Throw woex + Catch wex As Exception + If DownloadContentDefault_ProcessDownloadException() Then + v.Attempts += 1 + v.State = UStates.Missing + If MissingErrorsAdd Then ErrorDownloading(f, v.URL) + End If End Try Else v.State = UStates.Skipped @@ -1185,9 +1434,30 @@ BlockNullPicture: End Try End Sub Protected UseInternalM3U8Function As Boolean = False - Protected Overridable Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile) As SFile + Protected UseInternalM3U8Function_UseProgress As Boolean = False + Protected Overridable Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, + ByVal Token As CancellationToken) As SFile Return Nothing End Function + Protected UseInternalDownloadFileFunction As Boolean = False + Protected UseInternalDownloadFileFunction_UseProgress As Boolean = False + Protected Overridable Function DownloadFile(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, + ByVal Token As CancellationToken) As SFile + Return Nothing + End Function + Protected Overridable Function ValidateDownloadFile(ByVal URL As String, ByVal Media As UserMedia, ByRef Interrupt As Boolean) As Boolean + Return True + End Function + Protected Overridable Function DownloadContentDefault_GetRootDir() As String + Return MyFile.CutPath(IIf(IsSingleObjectDownload, 0, 1)).PathNoSeparator + End Function + Protected Overridable Sub DownloadContentDefault_PostProcessing(ByRef m As UserMedia, ByVal File As SFile, ByVal Token As CancellationToken) + End Sub + Protected Overridable Function DownloadContentDefault_ProcessDownloadException() As Boolean + Return True + End Function +#End Region +#Region "ProcessException" Protected Const EXCEPTION_OPERATION_CANCELED As Integer = -1 ''' Request DownloadingException ''' 0 - exit @@ -1208,14 +1478,19 @@ BlockNullPicture: End Function ''' 0 - Execute LogError and set HasError Protected MustOverride Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, Optional ByVal EObj As Object = Nothing) As Integer - Protected Function ChangeFileNameByProvider(ByVal f As SFile, ByVal m As UserMedia) As SFile +#End Region +#Region "ChangeFileNameByProvider, RunScript" + Protected Overridable Function CreateFileFromUrl(ByVal URL As String) As SFile + Return New SFile(URL) + End Function + Protected Overridable Function ChangeFileNameByProvider(ByVal f As SFile, ByVal m As UserMedia) As SFile Dim ff As SFile = Nothing Try If f.Exists Then If Not Settings.FileReplaceNameByDate.Value = FileNameReplaceMode.None Then ff = f ff.Name = String.Format(FileDateAppenderPattern, f.Name, CStr(AConvert(Of String)(If(m.Post.Date, Now), FileDateAppenderProvider, String.Empty))) - ff = SFile.Indexed_IndexFile(ff,, New NumberedFile(ff)) + ff = SFile.IndexReindex(ff,,, New NumberedFile(ff)) End If If Not ff.Name.IsEmptyString Then My.Computer.FileSystem.RenameFile(f, ff.File) : Return ff End If @@ -1238,7 +1513,7 @@ BlockNullPicture: If Not ScriptPattern.IsEmptyString Then If Not ScriptPattern.Contains(spa) Then ScriptPattern &= $" ""{spa}""" Using b As New BatchExecutor With {.RedirectStandardError = True} - b.Execute({String.Format(ScriptPattern, MyFile.CutPath(1).PathNoSeparator)}, EDP.SendInLog + EDP.ThrowException) + b.Execute({String.Format(ScriptPattern, MyFile.CutPath(1).PathNoSeparator)}, EDP.SendToLog + EDP.ThrowException) If b.HasError Or Not b.ErrorOutput.IsEmptyString Then Throw New Exception(b.ErrorOutput, b.ErrorException) End Using End If @@ -1248,6 +1523,7 @@ BlockNullPicture: End Try End Sub #End Region +#End Region #Region "Delete, Move, Merge, Copy" 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) @@ -1359,7 +1635,7 @@ BlockNullPicture: FilesMover.Invoke If SFile.GetFiles(UserBefore.File.CutPath,, SearchOption.AllDirectories, New ErrorsDescriber(False, False, False, New List(Of SFile))).Count = 0 Then - UserBefore.File.CutPath.Delete(SFO.Path, Settings.DeleteMode, EDP.SendInLog) + UserBefore.File.CutPath.Delete(SFO.Path, Settings.DeleteMode, EDP.SendToLog) End If If Not ScriptData.IsEmptyString AndAlso ScriptData.Contains(UserBefore.File.PathNoSeparator) Then _ ScriptData = ScriptData.Replace(UserBefore.File.PathNoSeparator, MyFile.PathNoSeparator) @@ -1424,7 +1700,7 @@ BlockNullPicture: #End Region #Region "Errors functions" Protected Sub LogError(ByVal ex As Exception, ByVal Message As String) - ErrorsDescriber.Execute(EDP.SendInLog, ex, $"{ToStringForLog()}: {Message}") + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"{ToStringForLog()}: {Message}") End Sub Protected Sub ErrorDownloading(ByVal f As SFile, ByVal URL As String) If Not f.Exists Then MyMainLOG = $"Error downloading from [{URL}] to [{f}]" @@ -1546,7 +1822,7 @@ BlockNullPicture: #Region "Base interfaces" Friend Interface IContentProvider ReadOnly Property Site As String - Property Name As String + ReadOnly Property Name As String Property ID As String Property FriendlyName As String Property Description As String @@ -1554,6 +1830,7 @@ BlockNullPicture: Property Temporary As Boolean Sub OpenSite(Optional ByVal e As ErrorsDescriber = Nothing) Sub DownloadData(ByVal Token As CancellationToken) + Sub DownloadSingleObject(ByVal Data As YouTube.Objects.IYouTubeMediaContainer, ByVal Token As CancellationToken) End Interface Friend Interface IUserData : Inherits IContentProvider, IComparable(Of UserDataBase), IComparable, IEquatable(Of UserDataBase), IIndexable, IDisposable Event UserUpdated(ByVal User As IUserData) @@ -1571,7 +1848,6 @@ BlockNullPicture: ReadOnly Property IsVirtual As Boolean ReadOnly Property Labels As List(Of String) #End Region - ReadOnly Property IsChannel As Boolean Property Exists As Boolean Property Suspended As Boolean Property ReadyForDownload As Boolean diff --git a/SCrawler/API/BaseObjects/DomainEnvir.vb b/SCrawler/API/BaseObjects/DomainEnvir.vb deleted file mode 100644 index 8df1ee5..0000000 --- a/SCrawler/API/BaseObjects/DomainEnvir.vb +++ /dev/null @@ -1,86 +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 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.Item = 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.Item, 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), - .DesignXMLNodeName = 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/BaseObjects/DomainsContainer.vb b/SCrawler/API/BaseObjects/DomainsContainer.vb new file mode 100644 index 0000000..023f132 --- /dev/null +++ b/SCrawler/API/BaseObjects/DomainsContainer.vb @@ -0,0 +1,108 @@ +' 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.Plugin +Imports PersonalUtilities.Forms +Imports PersonalUtilities.Tools +Imports ADB = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons +Namespace API.Base + Friend Class DomainsContainer : Implements IEnumerable(Of String), IMyEnumerator(Of String) + Friend Event DomainsUpdated(ByVal Sender As DomainsContainer) + Friend ReadOnly Property Domains As List(Of String) + Friend ReadOnly Property DomainsTemp As List(Of String) + Friend ReadOnly Property DomainsDefault As String + Friend Property Changed As Boolean + Private DomainsUpdateInProgress As Boolean = False + Friend Property UpdatedBySite As Boolean + Protected ReadOnly Property Instance As ISiteSettings + Friend Property DestinationProp As PropertyValue + Default Friend ReadOnly Property Item(ByVal Index As Integer) As String Implements IMyEnumerator(Of String).MyEnumeratorObject + Get + Return Domains(Index) + End Get + End Property + Friend ReadOnly Property Count As Integer Implements IMyEnumerator(Of String).MyEnumeratorCount + Get + Return Domains.Count + End Get + End Property + Friend Sub New(ByVal _Instance As ISiteSettings, ByVal DefaultValue As String) + Domains = New List(Of String) + DomainsTemp = New List(Of String) + Instance = _Instance + DomainsDefault = DefaultValue + If Not DomainsDefault.IsEmptyString Then Domains.ListAddList(CStr(DomainsDefault).Split("|"), LAP.NotContainsOnly) + End Sub + Friend Sub PopulateInitialDomains(ByVal InitialValue As String) + If Not InitialValue.IsEmptyString Then Domains.ListAddList(CStr(InitialValue).Split("|"), LAP.NotContainsOnly) + End Sub + Public Overrides Function ToString() As String + Return Domains.ListToString("|") + End Function + Friend Sub Add(ByVal NewDomains As IEnumerable(Of String), ByVal UpdateBySite As Boolean) + If Not DomainsUpdateInProgress Then + DomainsUpdateInProgress = True + Domains.ListAddList(NewDomains, LAP.NotContainsOnly) + If UpdateBySite Then Me.UpdatedBySite = True + Save() + DomainsUpdateInProgress = False + RaiseEvent DomainsUpdated(Me) + End If + End Sub + Friend Overridable Function Apply() As Boolean + If Changed Then + Domains.Clear() + Domains.ListAddList(DomainsTemp, LAP.NotContainsOnly) + Save() + RaiseEvent DomainsUpdated(Me) + Return True + Else + Return False + End If + End Function + Friend Overridable Sub Save() + If Not DestinationProp Is Nothing Then DestinationProp.Value = ToString() + End Sub + Friend Overridable Sub Reset() + Changed = False + DomainsTemp.Clear() + End Sub + Friend Overridable Sub OpenSettingsForm() + Dim __add As EventHandler(Of SimpleListFormEventArgs) = Sub(sender, e) e.Item = InputBoxE($"Enter a new domain using the pattern [{Instance.Site}.com]:", "New domain").IfNullOrEmptyE(Nothing) + Dim __delete As EventHandler(Of SimpleListFormEventArgs) = Sub(sender, e) + Dim n$ = AConvert(Of String)(e.Item, 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(Changed, DomainsTemp, Domains), Settings.Design) With { + .Buttons = {ADB.Add, ADB.Delete}, + .Mode = SimpleListFormModes.Remaining, + .FormText = Instance.Site, + .Icon = Instance.Icon, + .LocationOnly = True, + .Size = New Size(400, 330), + .DesignXMLNodeName = $"{Instance.Site}_DomainsForm" + } + AddHandler f.AddClick, __add + AddHandler f.DeleteClick, __delete + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then + Changed = True + DomainsTemp.Clear() + DomainsTemp.ListAddList(f.DataResult, LAP.NotContainsOnly) + End If + End Using + End Sub + Private Function GetEnumerator() As IEnumerator(Of String) Implements IEnumerable(Of String).GetEnumerator + Return New MyEnumerator(Of String)(Me) + End Function + Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator + Return GetEnumerator() + End Function + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/BaseObjects/InternalSettingsForm.Designer.vb b/SCrawler/API/BaseObjects/InternalSettingsForm.Designer.vb new file mode 100644 index 0000000..1097ec8 --- /dev/null +++ b/SCrawler/API/BaseObjects/InternalSettingsForm.Designer.vb @@ -0,0 +1,89 @@ +' 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.Base + + Partial Friend Class InternalSettingsForm : 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() + Me.components = New System.ComponentModel.Container() + Me.CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() + Me.TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + Me.TT_MAIN = New System.Windows.Forms.ToolTip(Me.components) + Me.CONTAINER_MAIN.ContentPanel.SuspendLayout() + Me.CONTAINER_MAIN.SuspendLayout() + Me.SuspendLayout() + ' + 'CONTAINER_MAIN + ' + ' + 'CONTAINER_MAIN.ContentPanel + ' + Me.CONTAINER_MAIN.ContentPanel.Controls.Add(Me.TP_MAIN) + Me.CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(184, 0) + Me.CONTAINER_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.CONTAINER_MAIN.LeftToolStripPanelVisible = False + Me.CONTAINER_MAIN.Location = New System.Drawing.Point(0, 0) + Me.CONTAINER_MAIN.Name = "CONTAINER_MAIN" + Me.CONTAINER_MAIN.RightToolStripPanelVisible = False + Me.CONTAINER_MAIN.Size = New System.Drawing.Size(184, 25) + Me.CONTAINER_MAIN.TabIndex = 0 + Me.CONTAINER_MAIN.TopToolStripPanelVisible = False + ' + 'TP_MAIN + ' + Me.TP_MAIN.CellBorderStyle = System.Windows.Forms.TableLayoutPanelCellBorderStyle.[Single] + Me.TP_MAIN.ColumnCount = 1 + Me.TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_MAIN.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + Me.TP_MAIN.Dock = System.Windows.Forms.DockStyle.Fill + Me.TP_MAIN.Location = New System.Drawing.Point(0, 0) + Me.TP_MAIN.Name = "TP_MAIN" + Me.TP_MAIN.RowCount = 1 + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100.0!)) + Me.TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 160.0!)) + Me.TP_MAIN.Size = New System.Drawing.Size(184, 0) + Me.TP_MAIN.TabIndex = 0 + ' + 'InternalSettingsForm + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(184, 25) + Me.Controls.Add(Me.CONTAINER_MAIN) + Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle + Me.KeyPreview = True + Me.MaximizeBox = False + Me.MinimizeBox = False + Me.Name = "InternalSettingsForm" + Me.ShowInTaskbar = False + Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide + Me.Text = "Settings" + Me.CONTAINER_MAIN.ContentPanel.ResumeLayout(False) + Me.CONTAINER_MAIN.ResumeLayout(False) + Me.CONTAINER_MAIN.PerformLayout() + Me.ResumeLayout(False) + + End Sub + + Private WithEvents TP_MAIN As TableLayoutPanel + Private WithEvents TT_MAIN As ToolTip + Private WithEvents CONTAINER_MAIN As ToolStripContainer + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/Download/VideosDownloaderForm.resx b/SCrawler/API/BaseObjects/InternalSettingsForm.resx similarity index 88% rename from SCrawler/Download/VideosDownloaderForm.resx rename to SCrawler/API/BaseObjects/InternalSettingsForm.resx index d0894be..0a1e786 100644 --- a/SCrawler/Download/VideosDownloaderForm.resx +++ b/SCrawler/API/BaseObjects/InternalSettingsForm.resx @@ -117,16 +117,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - False - - - False - - + 17, 17 - - 124, 17 - \ No newline at end of file diff --git a/SCrawler/API/BaseObjects/InternalSettingsForm.vb b/SCrawler/API/BaseObjects/InternalSettingsForm.vb new file mode 100644 index 0000000..0f84bee --- /dev/null +++ b/SCrawler/API/BaseObjects/InternalSettingsForm.vb @@ -0,0 +1,256 @@ +' 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.Reflection +Imports System.ComponentModel +Imports PersonalUtilities.Forms +Imports SCrawler.Plugin +Imports SCrawler.Plugin.Attributes +Namespace API.Base + Friend Class InternalSettingsForm + Private WithEvents MyDefs As DefaultFormOptions + Private ReadOnly Property MySettingsInstance As ISiteSettings + Private ReadOnly Property MyObject As Object + Private ReadOnly IsSettingsForm As Boolean = True + Private ReadOnly Property MyMembers As List(Of MemberOption) + ''' Default: 200 + Friend Property MinimumWidth As Integer = 200 + Private Class MemberOption : Inherits Hosts.PropertyValueHost : Implements IDisposable + Friend ToolTip As String + Friend Caption As String + Friend ThreeState As Boolean = False + Friend AllowNull As Boolean = True + Friend Provider As Type + Friend Overrides Property Type As Type + Get + Return _Type + End Get + Set(ByVal t As Type) + MyBase.Type = t + End Set + End Property + Friend Overrides ReadOnly Property Name As String + Get + Return Member.Name + End Get + End Property + Friend Overrides Property LeftOffset As Integer + Get + Return If(_LeftOffset, 0) + End Get + Set(ByVal NewOffset As Integer) + MyBase.LeftOffset = NewOffset + End Set + End Property + Private ReadOnly _MinimumWidth As Integer? = Nothing + Friend ReadOnly Property Width As Integer + Get + Return LeftOffset + If(_MinimumWidth, 0) + If(TypeOf Control Is CheckBox, 0, 200) + + PaddingE.GetOf({Control}).Horizontal(2) + MeasureText(Caption, Control.Font).Width + End Get + End Property + Friend OptName As String = String.Empty + Friend Sub New(ByRef PropertySource As Object, ByVal m As MemberInfo, ByVal ps As PSettingAttribute, ByVal po As PropertyOption) + Source = PropertySource + Member = m + _Type = Member.GetMemberType + _Value = Member.GetMemberValue(PropertySource) + With ps + ToolTip = .ToolTip + Caption = .Caption + ThreeState = .ThreeState + AllowNull = .AllowNull + Provider = .Provider + _LeftOffset = .LeftOffsetGet + ControlNumber = .Number + _MinimumWidth = .MinimumWidth + End With + If Not po Is Nothing Then + With po + OptName = po.Name + If ToolTip.IsEmptyString Then ToolTip = .ControlToolTip + If Caption.IsEmptyString Then Caption = .ControlText + End With + End If + End Sub + Protected Overrides ReadOnly Property Control_IsInformationLabel As Boolean + Get + Return False + End Get + End Property + Protected Overrides ReadOnly Property Control_ThreeStates As Boolean + Get + Return ThreeState + End Get + End Property + Protected Overrides ReadOnly Property Control_Caption As String + Get + Return Caption + End Get + End Property + Protected Overrides ReadOnly Property Control_ToolTip As String + Get + Return ToolTip + End Get + End Property + Friend Overloads Sub CreateControl(ByVal f As FieldsChecker, ByVal TT As ToolTip) + CreateControl(TT) + If Not Provider Is Nothing Then f.AddControl(Control, Caption, Type, AllowNull, Activator.CreateInstance(Provider)) + End Sub +#Region "IDisposable Support" + Private disposedValue As Boolean = False + Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean) + If Not disposedValue Then + If disposing Then Control.Dispose() + Control = Nothing + disposedValue = True + End If + End Sub + Protected Overrides Sub Finalize() + Dispose(False) + MyBase.Finalize() + End Sub + Friend Overloads Sub Dispose() Implements IDisposable.Dispose + Dispose(True) + GC.SuppressFinalize(Me) + End Sub +#End Region + End Class + Friend Sub New(ByVal Obj As Object, ByVal s As ISiteSettings, ByVal _IsSettingsForm As Boolean) + InitializeComponent() + MyDefs = New DefaultFormOptions(Me, Settings.Design) + MyMembers = New List(Of MemberOption) + + MyObject = Obj + MySettingsInstance = s + IsSettingsForm = _IsSettingsForm + If _IsSettingsForm Then + Text = "Settings" + Else + Text = "Options" + End If + Icon = s.Icon + End Sub + Private Sub InternalSettingsForm_Load(sender As Object, e As EventArgs) Handles Me.Load + Try + With MyDefs + .MyViewInitialize(True) + .AddOkCancelToolbar() + + Dim members As IEnumerable(Of MemberInfo) + Dim member As MemberInfo + Dim attr As PSettingAttribute + Dim opt As PropertyOption + Dim providersMembersSettings As IEnumerable(Of MemberInfo) + Dim providersMembersObj As IEnumerable(Of MemberInfo) + Dim providersPredicate As Func(Of MemberInfo, Boolean) = Function(m) m.MemberType = MemberTypes.Property AndAlso + m.GetMemberCustomAttributes(Of Provider).ListExists + Dim m1 As MemberInfo, m2 As MemberInfo + Dim tmpObj As Object + + members = GetObjectMembers(MyObject, Function(m) (m.MemberType = MemberTypes.Field Or m.MemberType = MemberTypes.Property) AndAlso + Not m.GetCustomAttribute(Of PSettingAttribute) Is Nothing) + providersMembersSettings = GetObjectMembers(MySettingsInstance, providersPredicate) + providersMembersObj = GetObjectMembers(MyObject, providersPredicate) + + If members.ListExists Then + For Each member In members + attr = member.GetMemberCustomAttribute(Of PSettingAttribute) + If Not attr Is Nothing AndAlso + (attr.Address = SettingAddress.Both OrElse + ( + (IsSettingsForm And attr.Address = SettingAddress.Settings) Or + (Not IsSettingsForm And attr.Address = SettingAddress.User) + ) + ) Then + opt = Nothing + + If Not attr.NameAssoc.IsEmptyString Then + m1 = GetObjectMembers(MyObject, Function(m) m.Name = attr.NameAssocInstance).FirstOrDefault + If Not m1 Is Nothing AndAlso (m1.MemberType = MemberTypes.Property Or + m1.MemberType = MemberTypes.Field Or + m1.MemberType = MemberTypes.Method) Then + tmpObj = m1.GetMemberValue(MyObject) + If Not tmpObj Is Nothing Then + m2 = GetObjectMembers(tmpObj, Function(m) m.Name = attr.NameAssoc).FirstOrDefault + If Not m2 Is Nothing Then opt = m2.GetMemberCustomAttribute(Of PropertyOption) + End If + End If + End If + + MyMembers.Add(New MemberOption(MyObject, member, attr, opt)) + End If + Next + End If + + .MyFieldsCheckerE = New FieldsChecker + + If MyMembers.Count > 0 Then + + Dim prov As IEnumerable(Of Provider) + Dim _prov As Provider + Dim si% = -1 + Dim i% + For Each provEnum In {providersMembersObj, providersMembersSettings} + si += 1 + If provEnum.ListExists Then + For Each member In provEnum + prov = member.GetMemberCustomAttributes(Of Provider) + If prov.ListExists Then + For Each _prov In prov + i = MyMembers.FindIndex(Function(m) If(si = 0, m.Name, m.OptName) = _prov.Name) + If i >= 0 Then MyMembers(i).SetProvider(member.GetMemberValue(If(si = 0, MyObject, CObj(MySettingsInstance))), _prov) + Next + End If + Next + End If + Next + + TP_MAIN.RowStyles.Clear() + TP_MAIN.RowCount = 0 + For i% = 0 To MyMembers.Count - 1 + With MyMembers(i) + .CreateControl(MyDefs.MyFieldsCheckerE, TT_MAIN) + TP_MAIN.RowStyles.Add(New RowStyle(SizeType.Absolute, .ControlHeight)) + TP_MAIN.RowCount += 1 + TP_MAIN.Controls.Add(.Control, 0, TP_MAIN.RowStyles.Count - 1) + End With + Next + Else + Throw New ArgumentOutOfRangeException("Members", "Settings instance does not contain settings members") + End If + + .MyFieldsChecker.EndLoaderOperations() + + Dim s As Size = Size + s.Height += (MyMembers.Sum(Function(m) m.ControlHeight) + + (PaddingE.GetOf({TP_MAIN},,,,,, 0).Vertical(MyMembers.Count - 1) / 2).RoundDown + MyMembers.Count - 1) + s.Width = MyMembers.Max(Function(m) m.Width) + PaddingE.GetOf({TP_MAIN, CONTAINER_MAIN, CONTAINER_MAIN.ContentPanel, Me}, False).Horizontal(2) + If MinimumWidth > 0 And s.Width < MinimumWidth Then s.Width = MinimumWidth + Size = s + MinimumSize = s + MaximumSize = s + + .EndLoaderOperations() + End With + Catch ex As Exception + MyDefs.InvokeLoaderError(ex) + End Try + End Sub + Private Sub InternalSettingsForm_Closing(sender As Object, e As CancelEventArgs) Handles Me.Closing + TP_MAIN.Controls.Clear() + MyMembers.ListClearDispose + End Sub + Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick + If MyDefs.MyFieldsChecker.AllParamsOK Then + MyMembers.ForEach(Sub(m) m.UpdateValueByControl()) + MyDefs.CloseForm() + End If + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Gfycat/Envir.vb b/SCrawler/API/Gfycat/Envir.vb index 482a883..88a5164 100644 --- a/SCrawler/API/Gfycat/Envir.vb +++ b/SCrawler/API/Gfycat/Envir.vb @@ -7,12 +7,70 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Net +Imports System.Threading Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects +Imports SCrawler.Plugin.Hosts Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Gfycat - Friend NotInheritable Class Envir - Private Sub New() + Friend NotInheritable Class Envir : Inherits UserDataBase + Friend Const SiteKey As String = "AndyProgram_Gfycat" + Friend Const SiteName As String = "Gfycat" + Friend Sub New() End Sub +#Region "UserDataBase Support" + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As PersonalUtilities.Functions.XML.XmlFile, ByVal Loading As Boolean) + End Sub + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + End Sub + Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) + SeparateVideoFolder = False + DownloadContentDefault(Token) + End Sub + Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, + Optional ByVal EObj As Object = Nothing) As Integer + Return 0 + End Function +#End Region +#Region "DownloadSingleObject" + Private _IsRedGifs As Boolean = False + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim urlVideo$ = GetVideo(Data.URL) + If Not urlVideo.IsEmptyString Then + If urlVideo.Contains("redgifs.com") Then + _IsRedGifs = True + DirectCast(Settings(RedGifs.RedGifsSiteKey).Source, RedGifs.SiteSettings).UpdateTokenIfRequired() + Dim newData As IYouTubeMediaContainer = Settings(RedGifs.RedGifsSiteKey).GetSingleMediaInstance(urlVideo, Data.File) + If Not newData Is Nothing Then + newData.Progress = Data.Progress + newData.Download(Data.UseCookies, Token) + YouTubeMediaContainerBase.Update(newData, Data) + DirectCast(Data, DownloadableMediaHost).ExchangeData(newData, Data) + With DirectCast(Data, YouTubeMediaContainerBase) + .Site = RedGifs.RedGifsSite + .SiteKey = RedGifs.RedGifsSiteKey + .SiteIcon = Settings(RedGifs.RedGifsSiteKey).Source.Image + End With + Else + Throw New Exception($"Unable to get RedGifs instance{vbCr}{Data.URL}{vbCr}{urlVideo}") + End If + Else + Dim m As New UserMedia(urlVideo, UserMedia.Types.Video) With {.URL_BASE = Data.URL} + m.File.Path = Data.File.Path + _TempMediaList.Add(m) + End If + End If + End Sub + Protected Overrides Sub DownloadSingleObject_CreateMedia(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + If Not _IsRedGifs Then MyBase.DownloadSingleObject_CreateMedia(Data, Token) + End Sub + Protected Overrides Sub DownloadSingleObject_PostProcessing(ByVal Data As IYouTubeMediaContainer, Optional ByVal ResetTitle As Boolean = True) + If Not _IsRedGifs Then MyBase.DownloadSingleObject_PostProcessing(Data, ResetTitle) + End Sub + Protected Overrides Sub DownloadSingleObject_Download(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + If Not _IsRedGifs Then MyBase.DownloadSingleObject_Download(Data, Token) + End Sub +#End Region Friend Shared Function GetVideo(ByVal URL As String) As String Try Dim r$ @@ -33,14 +91,22 @@ Namespace API.Gfycat Dim e As EDP = EDP.ReturnValue If TypeOf ex Is WebException Then Dim obj As HttpWebResponse = TryCast(DirectCast(ex, WebException).Response, HttpWebResponse) - If Not If(obj?.StatusCode, HttpStatusCode.OK) = HttpStatusCode.NotFound Then e += EDP.SendInLog + If Not If(obj?.StatusCode, HttpStatusCode.OK) = HttpStatusCode.NotFound Then e += EDP.SendToLog End If Return ErrorsDescriber.Execute(e, ex, $"[API.Gfycat.Envir.GetVideo({URL})]", String.Empty) End Try End Function - Friend Shared Function GetVideoInfo(ByVal URL As String) As IEnumerable(Of UserMedia) - Dim u$ = GetVideo(URL) - Return If(u.IsEmptyString, Nothing, {New UserMedia(u, UserMedia.Types.Video)}) + Friend Shared Function GetSingleMediaInstance(ByVal URL As String, ByVal OutputFile As SFile) As IYouTubeMediaContainer + If Not URL.IsEmptyString AndAlso URL.Contains("gfycat") Then + Return New DownloadableMediaHost(URL, OutputFile) With { + .Instance = New Envir, + .Site = SiteName, + .SiteKey = SiteKey, + .SiteIcon = Nothing + } + Else + Return Nothing + End If End Function 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 b9b7d09..9d47c2b 100644 --- a/SCrawler/API/Imgur/Envir.vb +++ b/SCrawler/API/Imgur/Envir.vb @@ -7,8 +7,11 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Net +Imports System.Threading Imports SCrawler.API.Base Imports SCrawler.API.Imgur.Declarations +Imports SCrawler.API.YouTube.Objects +Imports SCrawler.Plugin.Hosts Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.Documents.JSON @@ -18,8 +21,28 @@ Namespace API.Imgur Friend ReadOnly PostRegex As RParams = RParams.DMS("/([^/]+?)(|#.*?|\.[\w]{0,4})(|\?.*?)\Z", 1) End Module End Namespace - Friend NotInheritable Class Envir - Private Sub New() + Friend NotInheritable Class Envir : Inherits UserDataBase + Friend Const SiteKey As String = "AndyProgram_Imgur" + Friend Const SiteName As String = "Imgur" + Friend Sub New() + End Sub +#Region "UserDataBase Support" + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + End Sub + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + End Sub + Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) + SeparateVideoFolder = False + DownloadContentDefault(Token) + End Sub + Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, + Optional ByVal EObj As Object = Nothing) As Integer + Return 0 + End Function +#End Region + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim videos As IEnumerable(Of UserMedia) = GetVideoInfo(Data.URL, EDP.SendToLog) + If videos.ListExists Then _TempMediaList.AddRange(videos) End Sub Friend Shared Function GetGallery(ByVal URL As String, Optional ByVal e As ErrorsDescriber = Nothing) As List(Of String) Try @@ -47,7 +70,7 @@ Namespace API.Imgur End If Return Nothing Catch ex As Exception - Return DownloadingException(ex, $"[API.Imgur.Envir.GetGallery({URL})]", Nothing, e) + Return DownloadingException_Internal(ex, $"[API.Imgur.Envir.GetGallery({URL})]", Nothing, e) End Try End Function Friend Shared Function GetImage(ByVal URL As String, Optional ByVal e As ErrorsDescriber = Nothing) As String @@ -64,7 +87,7 @@ Namespace API.Imgur End If Return String.Empty Catch ex As Exception - Return DownloadingException(ex, $"[API.Imgur.Envir.GetImage({URL})]", String.Empty, e) + Return DownloadingException_Internal(ex, $"[API.Imgur.Envir.GetImage({URL})]", String.Empty, e) End Try End Function Friend Shared Function GetVideoInfo(ByVal URL As String, Optional ByVal e As ErrorsDescriber = Nothing) As IEnumerable(Of UserMedia) @@ -81,11 +104,23 @@ Namespace API.Imgur Return Nothing Catch ex As Exception If Not e.Exists Then e = EDP.LogMessageValue - Return ErrorsDescriber.Execute(e, ex, "Imgur standalone downloader: fetch media error") + Return ErrorsDescriber.Execute(e, ex, $"[API.Imgur.Envir.GetVideoInfo({URL})]: fetch media error") End Try End Function - Private Shared Function DownloadingException(ByVal ex As Exception, ByVal Message As String, - ByVal NullArg As Object, ByVal e As ErrorsDescriber) As Object + Friend Shared Function GetSingleMediaInstance(ByVal URL As String, ByVal OutputFile As SFile) As IYouTubeMediaContainer + If Not URL.IsEmptyString AndAlso URL.Contains("imgur.com") Then + Return New DownloadableMediaHost(URL, OutputFile) With { + .Instance = New Envir, + .Site = SiteName, + .SiteKey = SiteKey, + .SiteIcon = Nothing + } + Else + Return Nothing + End If + End Function + Private Shared Function DownloadingException_Internal(ByVal ex As Exception, ByVal Message As String, + ByVal NullArg As Object, ByVal e As ErrorsDescriber) As Object If TypeOf ex Is WebException Then Dim obj As HttpWebResponse = TryCast(DirectCast(ex, WebException).Response, HttpWebResponse) If Not obj Is Nothing Then @@ -97,7 +132,7 @@ Namespace API.Imgur End If End If End If - If Not e.Exists Then e = New ErrorsDescriber(EDP.ReturnValue + EDP.SendInLog) + If Not e.Exists Then e = New ErrorsDescriber(EDP.ReturnValue + EDP.SendToLog) Return ErrorsDescriber.Execute(e, ex, Message, NullArg) End Function End Class diff --git a/SCrawler/API/Instagram/Declarations.vb b/SCrawler/API/Instagram/Declarations.vb index a8853b3..93b46d5 100644 --- a/SCrawler/API/Instagram/Declarations.vb +++ b/SCrawler/API/Instagram/Declarations.vb @@ -6,16 +6,15 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY -Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Cookies Imports PersonalUtilities.Tools.Web.Clients Imports PersonalUtilities.Tools.Web.Clients.EventArguments -Imports PersonalUtilities.Tools.Web.Cookies +Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Instagram Friend Module Declarations Friend Const InstagramSite As String = "Instagram" Friend Const InstagramSiteKey As String = "AndyProgram_Instagram" Friend ReadOnly FilesPattern As RParams = RParams.DMS(".+?([^/\?]+?\.[\w\d]{3,4})(?=(\?|\Z))", 1, EDP.ReturnValue) - Friend ReadOnly Property DateProvider As New CustomProvider(Function(v, d, p, n, e) ADateTime.ParseUnicode(v)) Friend Sub UpdateResponser(ByVal Source As IResponse, ByRef Destination As Responser) Const r_wwwClaimName$ = "x-ig-set-www-claim" Const r_tokenName$ = "csrftoken" @@ -46,7 +45,7 @@ Namespace API.Instagram If Not wwwClaim.IsEmptyString Then Destination.Headers.Add(SiteSettings.Header_IG_WWW_CLAIM, wwwClaim) If Not token.IsEmptyString Then Destination.Headers.Add(SiteSettings.Header_CSRF_TOKEN, token) If Not isInternal Then - Destination.Cookies.Update(Source.Cookies, CookieKeeper.UpdateModes.ReplaceByNameAll, False, EDP.SendInLog) + Destination.Cookies.Update(Source.Cookies, CookieKeeper.UpdateModes.ReplaceByNameAll, False, EDP.SendToLog) Destination.SaveSettings() End If End If diff --git a/SCrawler/API/Instagram/EditorExchangeOptions.vb b/SCrawler/API/Instagram/EditorExchangeOptions.vb index a41a067..b84ed27 100644 --- a/SCrawler/API/Instagram/EditorExchangeOptions.vb +++ b/SCrawler/API/Instagram/EditorExchangeOptions.vb @@ -6,14 +6,24 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY -Imports SCrawler.Plugin +Imports SCrawler.Plugin.Attributes Namespace API.Instagram Friend Class EditorExchangeOptions + Friend Property GetTimeline As Boolean + Friend Property GetStories As Boolean + Friend Property GetTagged As Boolean - Friend Sub New(ByVal h As ISiteSettings) - With DirectCast(h, SiteSettings) + Friend Sub New(ByVal u As UserData) + With u + GetTimeline = .GetTimeline + GetStories = .GetStories + GetTagged = .GetTaggedData + End With + End Sub + Friend Sub New(ByVal s As SiteSettings) + With s GetTimeline = CBool(.GetTimeline.Value) GetStories = CBool(.GetStories.Value) GetTagged = CBool(.GetTagged.Value) diff --git a/SCrawler/API/Instagram/OptionsForm.Designer.vb b/SCrawler/API/Instagram/OptionsForm.Designer.vb deleted file mode 100644 index 6c59cb1..0000000 --- a/SCrawler/API/Instagram/OptionsForm.Designer.vb +++ /dev/null @@ -1,135 +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 -Namespace API.Instagram - - 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_GET_STORIES = New System.Windows.Forms.CheckBox() - Me.CH_GET_TAGGED = New System.Windows.Forms.CheckBox() - Me.CH_GET_TIMELINE = 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(260, 79) - 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(260, 104) - 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_GET_STORIES, 0, 1) - TP_MAIN.Controls.Add(Me.CH_GET_TAGGED, 0, 2) - TP_MAIN.Controls.Add(Me.CH_GET_TIMELINE, 0, 0) - 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 = 4 - 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.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(260, 79) - TP_MAIN.TabIndex = 0 - ' - 'CH_GET_STORIES - ' - Me.CH_GET_STORIES.AutoSize = True - Me.CH_GET_STORIES.Dock = System.Windows.Forms.DockStyle.Fill - Me.CH_GET_STORIES.Location = New System.Drawing.Point(4, 30) - Me.CH_GET_STORIES.Name = "CH_GET_STORIES" - Me.CH_GET_STORIES.Size = New System.Drawing.Size(252, 19) - Me.CH_GET_STORIES.TabIndex = 1 - Me.CH_GET_STORIES.Text = "Get stories" - Me.CH_GET_STORIES.UseVisualStyleBackColor = True - ' - 'CH_GET_TAGGED - ' - Me.CH_GET_TAGGED.AutoSize = True - Me.CH_GET_TAGGED.Dock = System.Windows.Forms.DockStyle.Fill - Me.CH_GET_TAGGED.Location = New System.Drawing.Point(4, 56) - Me.CH_GET_TAGGED.Name = "CH_GET_TAGGED" - Me.CH_GET_TAGGED.Size = New System.Drawing.Size(252, 19) - Me.CH_GET_TAGGED.TabIndex = 2 - Me.CH_GET_TAGGED.Text = "Get tagged data" - Me.CH_GET_TAGGED.UseVisualStyleBackColor = True - ' - 'CH_GET_TIMELINE - ' - Me.CH_GET_TIMELINE.AutoSize = True - Me.CH_GET_TIMELINE.Dock = System.Windows.Forms.DockStyle.Fill - Me.CH_GET_TIMELINE.Location = New System.Drawing.Point(4, 4) - Me.CH_GET_TIMELINE.Name = "CH_GET_TIMELINE" - Me.CH_GET_TIMELINE.Size = New System.Drawing.Size(252, 19) - Me.CH_GET_TIMELINE.TabIndex = 0 - Me.CH_GET_TIMELINE.Text = "Get Timeline" - Me.CH_GET_TIMELINE.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(260, 104) - 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(276, 143) - Me.MinimizeBox = False - Me.MinimumSize = New System.Drawing.Size(276, 143) - Me.Name = "OptionsForm" - Me.ShowInTaskbar = False - 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_GET_STORIES As CheckBox - Private WithEvents CH_GET_TAGGED As CheckBox - Private WithEvents CH_GET_TIMELINE As CheckBox - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/Instagram/OptionsForm.vb b/SCrawler/API/Instagram/OptionsForm.vb deleted file mode 100644 index b220363..0000000 --- a/SCrawler/API/Instagram/OptionsForm.vb +++ /dev/null @@ -1,40 +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 -Namespace API.Instagram - Friend Class OptionsForm - Private WithEvents MyDefs As DefaultFormOptions - Private ReadOnly Property MyExchangeOptions As EditorExchangeOptions - Friend Sub New(ByRef ExchangeOptions As EditorExchangeOptions) - InitializeComponent() - MyExchangeOptions = ExchangeOptions - MyDefs = New DefaultFormOptions(Me, Settings.Design) - End Sub - Private Sub OptionsForm_Load(sender As Object, e As EventArgs) Handles Me.Load - With MyDefs - .MyViewInitialize(True) - .AddOkCancelToolbar() - With MyExchangeOptions - CH_GET_TIMELINE.Checked = .GetTimeline - CH_GET_STORIES.Checked = .GetStories - CH_GET_TAGGED.Checked = .GetTagged - End With - .EndLoaderOperations() - End With - End Sub - Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick - With MyExchangeOptions - .GetTimeline = CH_GET_TIMELINE.Checked - .GetStories = CH_GET_STORIES.Checked - .GetTagged = CH_GET_TAGGED.Checked - End With - MyDefs.CloseForm() - End Sub - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/Instagram/SiteSettings.vb b/SCrawler/API/Instagram/SiteSettings.vb index 78046d0..78a8df7 100644 --- a/SCrawler/API/Instagram/SiteSettings.vb +++ b/SCrawler/API/Instagram/SiteSettings.vb @@ -34,49 +34,39 @@ Namespace API.Instagram End Property #End Region #Region "Providers" - Private Class TimersChecker : Implements IFieldsCheckerProvider - Private Property ErrorMessage As String Implements IFieldsCheckerProvider.ErrorMessage - Private Property Name As String Implements IFieldsCheckerProvider.Name - Private Property TypeError As Boolean Implements IFieldsCheckerProvider.TypeError + Private Class TimersChecker : Inherits FieldsCheckerProviderBase Private ReadOnly LVProvider As New ANumbers With {.FormatOptions = ANumbers.Options.GroupIntegral} Private ReadOnly _LowestValue As Integer Friend Sub New(ByVal LowestValue As Integer) _LowestValue = LowestValue End Sub - Private Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, - Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Implements ICustomProvider.Convert + Public Overrides Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object TypeError = False ErrorMessage = String.Empty If Not ACheck(Of Integer)(Value) Then TypeError = True ElseIf CInt(Value) < _LowestValue Then ErrorMessage = $"The value of [{Name}] field must be greater than or equal to {_LowestValue.NumToString(LVProvider)}" + HasError = True Else Return Value End If Return Nothing End Function - Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat - Throw New NotImplementedException("[GetFormat] is not available in the context of [TimersChecker]") - End Function End Class - Private Class TaggedNotifyLimitChecker : Implements IFieldsCheckerProvider - Private Property ErrorMessage As String Implements IFieldsCheckerProvider.ErrorMessage - Private Property Name As String Implements IFieldsCheckerProvider.Name - Private Property TypeError As Boolean Implements IFieldsCheckerProvider.TypeError - Private Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, - Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Implements ICustomProvider.Convert + Private Class TaggedNotifyLimitChecker : Inherits FieldsCheckerProviderBase + Public Overrides Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Dim v% = AConvert(Of Integer)(Value, -10) If v > 0 Or v = -1 Then Return Value Else ErrorMessage = $"The value of [{Name}] field must be greater than 0 or equal to -1" + HasError = True Return Nothing End If End Function - Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat - Throw New NotImplementedException("[GetFormat] is not available in the context of [TaggedNotifyLimitChecker]") - End Function End Class #End Region #Region "Authorization properties" @@ -270,17 +260,11 @@ Namespace API.Instagram Return False End Function #End Region -#Region "Plugin functions" +#Region "GetInstance" Friend Overrides Function GetInstance(ByVal What As Download) As IPluginContentProvider - Select Case What - Case Download.Main : Return New UserData - Case Download.SavedPosts - Dim u As New UserData - DirectCast(u, UserDataBase).User = New UserInfo With {.Name = Site} - Return u - End Select - Return Nothing + Return New UserData End Function +#End Region #Region "Downloading" Friend Property SkipUntilNextSession As Boolean = False Friend Overrides Function ReadyToDownload(ByVal What As Download) As Boolean @@ -328,13 +312,11 @@ Namespace API.Instagram SkipUntilNextSession = False End Sub #End Region - 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 +#Region "UserOptions, GetUserPostUrl" Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) If Options Is Nothing OrElse Not TypeOf Options Is EditorExchangeOptions Then Options = New EditorExchangeOptions(Me) If OpenForm Then - Using f As New OptionsForm(Options) : f.ShowDialog() : End Using + Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using End If End Sub Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String @@ -342,7 +324,7 @@ Namespace API.Instagram Dim code$ = DirectCast(User, UserData).GetPostCodeById(Media.Post.ID) If Not code.IsEmptyString Then Return $"https://instagram.com/p/{code}/" Else Return String.Empty Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "Can't open user's post", String.Empty) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "Can't open user's post", String.Empty) End Try End Function #End Region diff --git a/SCrawler/API/Instagram/UserData.vb b/SCrawler/API/Instagram/UserData.vb index 3eb9bf6..ad78fdd 100644 --- a/SCrawler/API/Instagram/UserData.vb +++ b/SCrawler/API/Instagram/UserData.vb @@ -8,13 +8,14 @@ ' but WITHOUT ANY WARRANTY Imports System.Net Imports System.Threading +Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.XML.Base Imports PersonalUtilities.Functions.Messaging Imports PersonalUtilities.Functions.RegularExpressions 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 Friend Class UserData : Inherits UserDataBase @@ -77,7 +78,7 @@ Namespace API.Instagram #End Region #Region "Exchange options" Friend Overrides Function ExchangeOptionsGet() As Object - Return New EditorExchangeOptions(HOST.Source) With {.GetTimeline = GetTimeline, .GetStories = GetStories, .GetTagged = GetTaggedData} + Return New EditorExchangeOptions(Me) End Function Friend Overrides Sub ExchangeOptionsSet(ByVal Obj As Object) If Not Obj Is Nothing AndAlso TypeOf Obj Is EditorExchangeOptions Then @@ -139,7 +140,7 @@ Namespace API.Instagram x = New XmlFile With {.AllowSameNames = True} x.AddRange(PostsKVIDs) x.Name = "Posts" - x.Save(f, EDP.SendInLog) + x.Save(f, EDP.SendToLog) x.Dispose() End If End If @@ -182,7 +183,7 @@ Namespace API.Instagram End If Return String.Empty Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"{ToStringForLog()}: Cannot find post code by ID ({PostID})", String.Empty) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, $"{ToStringForLog()}: Cannot find post code by ID ({PostID})", String.Empty) End Try End Function Private Function GetPostIdBySection(ByVal ID As String, ByVal Section As Sections) As String @@ -621,7 +622,7 @@ Namespace API.Instagram PostsKVIDs.ListAddValue(PostIDKV, LNC) PostDate = .Value("taken_at") If Not IsSavedPosts Then - Select Case CheckDatesLimit(PostDate, DateProvider) + Select Case CheckDatesLimit(PostDate, UnixDate32Provider) Case DateResult.Skip : Continue For Case DateResult.Exit : If Not Pinned Then Return False End Select @@ -637,7 +638,7 @@ Namespace API.Instagram End Function #End Region #Region "Code ID converters" - Private Shared Function CodeToID(ByVal Code As String) As String + Private Function CodeToID(ByVal Code As String) As String Const CodeSymbols$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" Try If Not Code.IsEmptyString Then @@ -652,13 +653,13 @@ Namespace API.Instagram Return String.Empty End If Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[API.Instagram.UserData.CodeToID({Code})", String.Empty) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, $"[API.Instagram.UserData.CodeToID({Code})", String.Empty) End Try End Function #End Region #Region "Obtain Media" Private Sub ObtainMedia(ByVal n As EContainer, ByVal PostID As String, Optional ByVal SpecialFolder As String = Nothing, - Optional ByVal DateObj As String = Nothing) + Optional ByVal DateObj As String = Nothing) Try Dim img As Predicate(Of EContainer) = Function(_img) Not _img.Name.IsEmptyString AndAlso _img.Name.StartsWith("image_versions") AndAlso _img.Count > 0 Dim vid As Predicate(Of EContainer) = Function(_vid) Not _vid.Name.IsEmptyString AndAlso _vid.Name.StartsWith("video_versions") AndAlso _vid.Count > 0 @@ -737,7 +738,7 @@ Namespace API.Instagram l.Clear() End If Catch ex As Exception - ErrorsDescriber.Execute(EDP.SendInLog, ex, "API.Instagram.ObtainMedia2") + ErrorsDescriber.Execute(EDP.SendToLog, ex, "API.Instagram.ObtainMedia2") End Try End Sub #End Region @@ -898,38 +899,25 @@ Namespace API.Instagram End Sub #End Region #Region "Create media" - Private Shared Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, - Optional ByVal SpecialFolder As String = Nothing) As UserMedia + Private Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, + Optional ByVal SpecialFolder As String = Nothing) As UserMedia _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) Dim m As New UserMedia(_URL, t) With {.Post = New UserPost With {.ID = PostID}} If Not m.URL.IsEmptyString Then m.File = CStr(RegexReplace(m.URL, FilesPattern)) - If Not PostDate.IsEmptyString Then m.Post.Date = AConvert(Of Date)(PostDate, DateProvider, Nothing) Else m.Post.Date = Nothing + If Not PostDate.IsEmptyString Then m.Post.Date = AConvert(Of Date)(PostDate, UnixDate32Provider, Nothing) Else m.Post.Date = Nothing m.SpecialFolder = SpecialFolder Return m End Function #End Region #Region "Standalone downloader" - Friend Shared Function GetVideoInfo(ByVal URL As String, ByVal r As Responser) As IEnumerable(Of UserMedia) - Try - If Not URL.IsEmptyString AndAlso URL.Contains("instagram.com") Then - Dim PID$ = RegexReplace(URL, RParams.DMS(".*?instagram.com/p/([_\w\d]+)", 1)) - If Not PID.IsEmptyString AndAlso Not ACheck(Of Long)(PID) Then PID = CodeToID(PID) - If Not PID.IsEmptyString Then - Using t As New UserData - t.SetEnvironment(Settings(InstagramSiteKey), Nothing, False, False) - t.Responser = New Responser - t.Responser.Copy(r) - t.PostsToReparse.Add(New PostKV With {.ID = PID}) - t.DownloadPosts(Nothing) - Return ListAddList(Nothing, t._TempMediaList) - End Using - End If - End If - Return Nothing - Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.ShowMainMsg + EDP.SendInLog, ex, $"Instagram standalone downloader: fetch media error ({URL})") - End Try - End Function + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim PID$ = RegexReplace(Data.URL, RParams.DMS(".*?instagram.com/p/([_\w\d]+)", 1)) + If Not PID.IsEmptyString AndAlso Not ACheck(Of Long)(PID) Then PID = CodeToID(PID) + If Not PID.IsEmptyString Then + PostsToReparse.Add(New PostKV With {.ID = PID}) + DownloadPosts(Token) + End If + End Sub #End Region #Region "IDisposable Support" Protected Overrides Sub Dispose(ByVal disposing As Boolean) diff --git a/SCrawler/API/LPSG/Declarations.vb b/SCrawler/API/LPSG/Declarations.vb index d7bc31f..1c5a36c 100644 --- a/SCrawler/API/LPSG/Declarations.vb +++ b/SCrawler/API/LPSG/Declarations.vb @@ -10,28 +10,41 @@ Imports SCrawler.API.Base Imports PersonalUtilities.Functions.RegularExpressions Namespace API.LPSG Friend Module Declarations - Friend ReadOnly Property PhotoRegEx As RParams = RParams.DM("(https://www.lpsg.com/attachments)(.+?)(?="")", 0, RegexReturn.List) + Friend ReadOnly Property PhotoRegEx As RParams = + RParams.DM("(?<=(https://www.lpsg.com|)/attachments/)([^/]+?[-\.]{1}(jpg|jpeg|gif|png|webm)\.?\d*)(?=/?"")", 0, RegexReturn.List, + CType(Function(Input$) If(Input.IsEmptyString, String.Empty, $"https://www.lpsg.com/attachments/{Input.StringTrimStart("/")}"), + Func(Of String, String))) Friend ReadOnly Property PhotoRegExExt As New RParams("img.data.src=""(/proxy[^""]+?)""", Nothing, 1, RegexReturn.List) With { - .Converter = Function(Input) $"https://www.lpsg.com/{SymbolsConverter.HTML.Decode(Input)}"} + .Converter = Function(Input) $"https://www.lpsg.com/{SymbolsConverter.HTML.Decode(Input)}"} Friend ReadOnly Property NextPageRegex As RParams = RParams.DMS(" 0 Then Credentials.ListAddList(x, LAP.IgnoreICopier) + End Using + End If + End Sub + Friend Overrides Function Apply() As Boolean + If Changed Then + Credentials.Clear() + If CredentialsTemp.Count > 0 Then Credentials.AddRange(CredentialsTemp) + CredentialsTemp.Clear() + End If + Return MyBase.Apply() + End Function + Friend Overrides Sub Save() + If Credentials.Count > 0 Then + Using x As New XmlFile With {.AllowSameNames = True} + x.AddRange(Credentials) + x.Name = "DomainsCredentials" + x.Save(CredentialsFile) + End Using + Else + CredentialsFile.Delete(,, EDP.None) + End If + MyBase.Save() + End Sub + Friend Overrides Sub Reset() + CredentialsTemp.Clear() + MyBase.Reset() + End Sub + Friend Overrides Sub OpenSettingsForm() + Using f As New SettingsForm(Instance) + f.ShowDialog() + If f.DialogResult = DialogResult.OK Then + Changed = True + CredentialsTemp.Clear() + If f.MyCredentials.Count > 0 Then CredentialsTemp.AddRange(f.MyCredentials) + DomainsTemp.Clear() + If f.MyDomains.Count > 0 Then DomainsTemp.ListAddList(f.MyDomains, LAP.NotContainsOnly) + End If + End Using + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Mastodon/SettingsForm.Designer.vb b/SCrawler/API/Mastodon/SettingsForm.Designer.vb new file mode 100644 index 0000000..d1d6ad2 --- /dev/null +++ b/SCrawler/API/Mastodon/SettingsForm.Designer.vb @@ -0,0 +1,165 @@ +' 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.Mastodon + + 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 + Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel + Dim ActionButton1 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(SettingsForm)) + Dim ActionButton2 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton3 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton4 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton5 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Dim ActionButton6 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() + Me.CMB_DOMAINS = New PersonalUtilities.Forms.Controls.ComboBoxExtended() + Me.TXT_AUTH = New PersonalUtilities.Forms.Controls.TextBoxExtended() + Me.TXT_TOKEN = New PersonalUtilities.Forms.Controls.TextBoxExtended() + CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() + TP_MAIN = New System.Windows.Forms.TableLayoutPanel() + CONTAINER_MAIN.ContentPanel.SuspendLayout() + CONTAINER_MAIN.SuspendLayout() + TP_MAIN.SuspendLayout() + CType(Me.CMB_DOMAINS, System.ComponentModel.ISupportInitialize).BeginInit() + CType(Me.TXT_AUTH, System.ComponentModel.ISupportInitialize).BeginInit() + CType(Me.TXT_TOKEN, System.ComponentModel.ISupportInitialize).BeginInit() + Me.SuspendLayout() + ' + 'CONTAINER_MAIN + ' + ' + 'CONTAINER_MAIN.ContentPanel + ' + CONTAINER_MAIN.ContentPanel.Controls.Add(TP_MAIN) + CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(384, 361) + 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, 361) + 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.ColumnStyles.Add(New System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20.0!)) + TP_MAIN.Controls.Add(Me.CMB_DOMAINS, 0, 0) + TP_MAIN.Controls.Add(Me.TXT_AUTH, 0, 1) + TP_MAIN.Controls.Add(Me.TXT_TOKEN, 0, 2) + 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.Percent, 100.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.0!)) + TP_MAIN.Size = New System.Drawing.Size(384, 361) + TP_MAIN.TabIndex = 0 + ' + 'CMB_DOMAINS + ' + ActionButton1.BackgroundImage = CType(resources.GetObject("ActionButton1.BackgroundImage"), System.Drawing.Image) + ActionButton1.Name = "Add" + ActionButton1.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Add + ActionButton2.BackgroundImage = CType(resources.GetObject("ActionButton2.BackgroundImage"), System.Drawing.Image) + ActionButton2.Name = "Delete" + ActionButton2.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Delete + ActionButton3.BackgroundImage = CType(resources.GetObject("ActionButton3.BackgroundImage"), System.Drawing.Image) + ActionButton3.Name = "Clear" + ActionButton3.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + ActionButton4.BackgroundImage = CType(resources.GetObject("ActionButton4.BackgroundImage"), System.Drawing.Image) + ActionButton4.Name = "ArrowDown" + ActionButton4.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.ArrowDown + ActionButton4.Visible = False + Me.CMB_DOMAINS.Buttons.Add(ActionButton1) + Me.CMB_DOMAINS.Buttons.Add(ActionButton2) + Me.CMB_DOMAINS.Buttons.Add(ActionButton3) + Me.CMB_DOMAINS.Buttons.Add(ActionButton4) + Me.CMB_DOMAINS.Dock = System.Windows.Forms.DockStyle.Fill + Me.CMB_DOMAINS.ListDropDownStyle = PersonalUtilities.Forms.Controls.ComboBoxExtended.ListMode.Simple + Me.CMB_DOMAINS.Location = New System.Drawing.Point(4, 4) + Me.CMB_DOMAINS.Name = "CMB_DOMAINS" + Me.CMB_DOMAINS.Size = New System.Drawing.Size(378, 296) + Me.CMB_DOMAINS.TabIndex = 0 + ' + 'TXT_AUTH + ' + ActionButton5.BackgroundImage = CType(resources.GetObject("ActionButton5.BackgroundImage"), System.Drawing.Image) + ActionButton5.Name = "Clear" + ActionButton5.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_AUTH.Buttons.Add(ActionButton5) + Me.TXT_AUTH.CaptionText = "Auth" + Me.TXT_AUTH.CaptionToolTipEnabled = True + Me.TXT_AUTH.CaptionToolTipText = "Bearer token" + Me.TXT_AUTH.CaptionWidth = 50.0R + Me.TXT_AUTH.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_AUTH.Location = New System.Drawing.Point(4, 306) + Me.TXT_AUTH.Name = "TXT_AUTH" + Me.TXT_AUTH.Size = New System.Drawing.Size(376, 22) + Me.TXT_AUTH.TabIndex = 1 + ' + 'TXT_TOKEN + ' + ActionButton6.BackgroundImage = CType(resources.GetObject("ActionButton6.BackgroundImage"), System.Drawing.Image) + ActionButton6.Name = "Clear" + ActionButton6.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear + Me.TXT_TOKEN.Buttons.Add(ActionButton6) + Me.TXT_TOKEN.CaptionText = "Token" + Me.TXT_TOKEN.CaptionToolTipEnabled = True + Me.TXT_TOKEN.CaptionToolTipText = "csrf token" + Me.TXT_TOKEN.CaptionWidth = 50.0R + Me.TXT_TOKEN.Dock = System.Windows.Forms.DockStyle.Fill + Me.TXT_TOKEN.Location = New System.Drawing.Point(4, 335) + Me.TXT_TOKEN.Name = "TXT_TOKEN" + Me.TXT_TOKEN.Size = New System.Drawing.Size(376, 22) + Me.TXT_TOKEN.TabIndex = 2 + ' + '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, 361) + Me.Controls.Add(CONTAINER_MAIN) + Me.Icon = Global.SCrawler.My.Resources.SiteResources.MastodonIcon_48 + Me.MinimumSize = New System.Drawing.Size(400, 400) + Me.Name = "SettingsForm" + Me.ShowInTaskbar = False + Me.Text = "Mastodon domains" + CONTAINER_MAIN.ContentPanel.ResumeLayout(False) + CONTAINER_MAIN.ResumeLayout(False) + CONTAINER_MAIN.PerformLayout() + TP_MAIN.ResumeLayout(False) + CType(Me.CMB_DOMAINS, System.ComponentModel.ISupportInitialize).EndInit() + CType(Me.TXT_AUTH, System.ComponentModel.ISupportInitialize).EndInit() + CType(Me.TXT_TOKEN, System.ComponentModel.ISupportInitialize).EndInit() + Me.ResumeLayout(False) + + End Sub + Private WithEvents CMB_DOMAINS As PersonalUtilities.Forms.Controls.ComboBoxExtended + Private WithEvents TXT_AUTH As PersonalUtilities.Forms.Controls.TextBoxExtended + Private WithEvents TXT_TOKEN As PersonalUtilities.Forms.Controls.TextBoxExtended + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Mastodon/SettingsForm.resx b/SCrawler/API/Mastodon/SettingsForm.resx new file mode 100644 index 0000000..da953f2 --- /dev/null +++ b/SCrawler/API/Mastodon/SettingsForm.resx @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + False + + + False + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGOfPtRkwAAACBjSFJNAAB6 + JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAACXBIWXMAAAsTAAALEwEAmpwYAAADmUlE + QVRIS62WWWxMURjHL220JW1HausmlFrDFKUhnUGH6bRFzJ2idImlC0Vp2mlji1A8iNhCPIjIRES8EU+W + h2oEtbSDTk3HNNM7S01VKsXjkb/vXBo3k1Ee7sMvmZzzzf//ne/+z50RAAxL1MUIG4G/YAv3HSVhF5Vw + IYNdz3LadVj9RgdTB+HQYYPHIJuE1ocSdlEJFzG+1bPRLQLinglIeCkg+XUkKvz56hnkOfQs/rmA8S9H + YEp7FDI64tAQtKhnsMapZ7zzNHsUFnbGY4VzIk70l6hnIH4wsDR7NBZ3apDrSqL5T8eFgUr1DLZ78lim + Q4N8VzK29MxEpZSBa4M16hnU+c3M9CEFpdJsVHsXos63DDcHrf9nQEXD5VymwW/5USLNwl5vJhp7dTgW + NML2pR7jbsUMS+KdMTa5Q8NQxinfBU4dRFcOyjy52OtbhwOBDTgZLKPPmTgY0ON4MBdNfSbYBupxY8Aq + G10dqMG5/nIc7ytGQ6CQRliAamkTN/g1Ai4e95Qy3iogpX0UtBRDnhRzdxq2SXOxz5eFQ70rScCEU335 + ssGxj0YS06HSm4GN3ekwdE2C1hGH1LZR0JDOJof5jwHvnIvzTa0jlooTYfktvt+fhcOBHDQFTWRgxJGP + ObAGsulZLMLWnjlY756K5c4JmNcRi6T2SGheCIihS2l5ozAo6NRhMolnUAcGV6IcwwqvFrX+JTjYuwKH + SfRAYDms/mzs9y1GFe2VSnOw1j0FejqpLN4WCX4ZufiIBwLMLxQGm12rsLQzgWKYgmLPLNTQw6ynpDSS + IBet8y+TqaVRVdFIeJrWuCcj+/0EzH43BomvIhBLI45uFiDcJ+6QwROFwa6+Amb9bGFNg6Xs9Ncd7Oy3 + Knb2eyU7/20nu9y/m136tIvEl6BC0qKoZwby3alo9JVhj7T5R7m/kJVIIityi8zyXmTiW+I10SqyIQNb + uIgNwYuuf25kFd75KPKkI49OmUWnrfYWyXv/wBb2cijhhVf6a9lGei65XclYRDd6mj0GWz2iLBJaH0rY + RSVc5Eywmhm7kuQXHX+bJlBStrh+zTi0PpSwi0q4yNFAOVvgiEcKJWUsxZn/NhT+znlofShhF5VwkRpv + MUtti4KGYjj6sYCIh5QSu4oG27stjItHU+cjeQzvkcFzFQ2KnSKLoc4FukDCXeI2GbSoaFD4ziyPxNxK + 0AUyNxOP1DOwcaG/8I+/LRB+At7psBnyDBG0AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29m + dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAVoSURBVEhLhZVrTJNXGMdfrtNSQIoadKRz2o0CorU3 + WkDIVBRaaGNbwAteh+AARRQlitEYTTRekiX7sH3YPmyZH9wtziybigLRCWTaCW5sCBWhlrb0Ci9zSxbo + 2f+UliGX7SS/tO85z/k9T57zXhhCCPO7Wh3VIhB83JKQ0Nu4bNlHm5YseZ1hmHC69n+Y5HLFcz7/ft/S + pY+vr1hhwL4oEBJcZ0x793If5uZ+1VNfT/qvXCHP6+p8tzMymqRxcW8hMGKqbDo9MlmWddu2AfbiRTJ6 + +TIZKC52fyAUVi2JiYkLJmGaBYIPnx4+TPrOnCH9p08TC4LNx46RWwrF/ZXR0W/PleRZZuY669atZvbS + JcJiL9vQQEZPnSKmwkLPjcTE97GPB8KZlvh4C5X31dWRgRMniAVBtvPnyWB9ve+2XP7jmtjYpOlJTOnp + G60lJRZaOZWPQs4ePUpGUZh3xw7SnJDQhT0KEM3c5fOv9paVkX4kMAPL8ePEig1D584RG9rVpFS2rY6J + EQaTmKTSjbbiYsvIhQuERTGjKIrFvtHaWjK8fz9plsudexYu/BLxKsBj9ALBGzel0vt9e/b4XiBoENhQ + zRDOxIWWOY4cIS0KRZs4Nja5QyLJtRoM1pGzZ/0tYVExi/ayNTVkBPJ76enuJA7nM4j3gVWAHjgTIYqL + E96SStvMu3YR64EDxF5dTYYOHSJOJPNA5Kiu9rUrlZ1mrdbCnjzpr5jFGotYtqpqQi6TuVM4nKvwlYHU + gDzU31OMSGl8fPJtsbjVsn27z15RQRzAVVlJ3BB4kcx78CAZQbUjVIxrFtd+OdrbmpHhEXG5VE4rTwHz + wMRdFDw4jEgFj5dyRyRqsxYVEcfu3cQFPPv2ES8qHEbCYRzgsFZLvO+8Q7xKJXGDVoXCK46Ovob95YBW + Ph/8+xwE/wSTyHi81OZVq9qsGs2Ye8sW4srPJy6JhDgTE4kzOpo4IyKIMyyMOLhcX9Py5R4lj0cPtAKs + BBwwKfc7p174J5BEhHY9FIk6bBDaIRuiQkDFfsLDSbdU+pdBKPwe8e+BNDBD7vdNn6BYd+6stK5da7bP + nz9TDujcoEAw1lJY+CyFz9dCHDubnDJjwltRccS5fr3TjurnlIMBYE5NJY8Nhq7SrCwREsz6xL9y4S4v + b3Bt2uSyR0XNkDvQe9ouKu8HvaGh5FfQIxL5OgyG30qUStqmGUkm/3jKy0+48vLcs1XuiI8nL/Ly/rYl + JfmovCcgN4JW+l8iGe8oKuoqzcyckSQob3CpVB47l+sXv9KWxYtJt0r1x9ns7HZjQYHNnJxMfoH0EXgA + 7oFm0CmTjRsNhs6Na9bQF+Tkq57xlJXVu9Rqz9Bs8kWLSG9BwcsqieQONlXnpaaWdul0z7rR+6C8CTSC + m8Aol4+36/XGT7VaevCRIIRx6/WWoQULZq2cyveLxY0IrAT0IHm1OTmZT3Q6U2da2qT8B/Ad+BZ05OSM + GXW6p4hdBiIZZ1FRt5vPn6vyuwiqCsj9Xyq6qXbDBkWnXm/6OS3NN1X+dUgIeZSdPXZPoxlEXC6IY9pL + S7faNBqXC9Iplf95YBb5ZF+RpGbdunQcbO/D1avJ9YC8LT19/Iv8/BeqpKRPEDORAGNeY3HxSYtG43Eq + FL5etfpljUzWhPlZ5VOTlGVliR+hHUbs+0mpHP9GpRqM5XAuY20zmGgRRohYKIx9rNd/3qfTOa7l5uLu + C63BvARw6fp0eRCMyBslJe8+2bx58EFhoVMlFNJvgQ4kgggQEgykvV0ApEAd+J3z8Z8KxmuA3pr0zikA + b4LJZ2FqYBigFdOPNf0NC679Fxi0OPr+XxiAJgwURph/AJfOQQebMR8TAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAE65JREFUeF7t + 3X2sJWddB/DdLi2lQG2hdOHuvfM887J7Cxca4ELTQMDWKigIFpBAEAgi9g+CJpJo9Q8NJhgBiYZIYspL + GlAKCkhEC4KgQlsLQkqhKi/lrYWWlxaw3dLddrerz/Q89+7dc2fbfTn3npf5fJJv2rS758z85nnOzJz5 + nZktAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMK3O3r79wVUIz65jfGNVxI/VIX69CvGO9M//a9P+e8o3B/8v + vKn9s+3fyX8dAJgmaWd+fl3E96Wd/E9XdvZHkfbvXNa+Rn45AGCS3bvjj/E/h3box5OrmxjPyy8PAEyS + XXO7zqhCeH/HDnwUOdCE+J6zdux4eH47YIrEGE8uy/Ls9Bnx/LooL0oH9b9Th/I1TVG+rCqKC+q6Xsh/ + FJgmO8vy6WknfdPQTnsjckMdwlPy2wITLO3wF6si/lGas1ekuXvX0Fzuyg9S3psOCl6qDwimQB3ji9Ok + 3btmEm907kpnEa/Mbw9Mlq1pB/6cdHZ/ZcfcPZrcXoXyrVVVFfl1gUmSdsS/libqPUMTd5NSvjktwrbB + kgDjVi1UT26K+Nnu+XrMuaud60uPWHpIfhtg3JqyfEaanHcPTdZNTRPCPy4uLj40LxIwBudt2fKAtOP/ + 0zQnN+5koIg3tpca81sC49J+LZcm5a3rJulYEq6LSV40YBOFEB6V5uFV6+flRiTsSwf9r81vDYzBCSO4 + vjfq/KAuiqfm5QM2QRPjuWnubUbz71DCn6W33zpYCmDT1EX5m92Tcuy5q47xFXkxgQ3UduqnOXfn0Bzc + xJSvz4sCbIb2pzlp8v1w/WScnKSzkjekRT1hsMTAKC0vL5/Ydud3zb1NT1FelBcL2GiDm3d0TMTJy0ea + pjk1LzYwAu3NvtLc+uTQXBtn7tYYCJtja/vQno5JOJFpQrzWb4hhNJoQnpjm1Q3D82wCcnNRFKfnxQQ2 + Qttk1zH5JjzhFmcIcHzyzb6O5aFem5J0sP/OvKjARmg7b7sm3xRkT3vDorwawJHb1t6Ep2NOTVoOtDch + yssMjFr6IPh8x8SbnsT4lrQamgPhCMzPzz+sifHjnXNpMnN5XnRglJaWlk5KE2z/0ISbxnzQQ0bgvlXz + 1ePSXPnG0NyZ+DRF8Zi8CsCo7Azh0V0TbkrzRc2B0G3wIJ9429CcmZLce4MgYJTyff87JtzU5uayLM/J + qwcM7vD5+jQ3DgzNlWnKDXldgFFJZwW/2jHZpj1727uZ5VWE3mofqJXmw4eG5sdUpqqqXXm1gFGoQnhJ + 12SbgRxoYvzjtIruK04vxRjPSvPgK0PzYmqTPqtemVcNGIU6xgu7JtusJH1ovH9ubu6UvLrQC2ncPyuN + /58Mz4fpTvnmvHrAKJQL5dO6J9ssJXxucWFhLq8yzLKtaUf5h2ncb9zz+8eUKsYP53UERmHX/PyOrsk2 + g7nJDUWYZUuPWHpIE8oPdIz92UiMn86rCoxIOmOYta8KD5uftk2Peb1hZtTzdVOHcF3HmJ+ZVCF+Ia8u + MCppcl0+PNlmOG1zYPtYYc2BzIQ0np+ZxvWPh8b5LObqvMrAqEzRo4BHmctijCfnEsBUqkP5u2ksz8Kd + PI8g5SfyagOj0jbIpQk2c01DR5Brmh3NfC4DTI324LWO8V0dY3pm48mAsEGm7OEgo0sRb9wZ4+NzGWDi + lWUZ0ti9Zt1YnvUU8fdyCYBRmsFbAh9xqhDvqEN4Xi4FTKz8s93vD4/hPiSdpJyXywCMWPtrgKuGJ12P + ck/6gPmDXAuYOHVR/lY6UN3XMXb7kDv17MAGqhaqJ6WJ1sdegDUJ726a5oG5JDB2917vL+Kl3eO1N/lQ + LgewUdIO8E0dk69vubosy+25JDA2bYNuFeJnOsZovxLjhbkkwEZZXl4+0QfOvfl2Ogg4O5cFNl1dFE9N + 4/B7Q+Oyj7mh/VzKZQE2UtM0j6iL+LWOidizhN3OPBiHuigvSmPwrvVjsn9pQnh1LguwGQa3Fo3fHp6M + Pcw97c1WcllgQy0tLZ2UDr7/qmMc9jJNiF/WkwNjMHhQ0GzfX/yIU8RLfRCxkdq+kzTfrugcf/3MgZ1l + +fRcHmCztU8Yq2P8h47J2cdcpTmQjdCE8IQ0vnzjdkjKP8nlAcZoWxXin3dP0n4l1eGb9UL92FwXOG51 + Ub48ja09w2Otz2nvTJpKs21QIWDs0lnKb6TJqTEphN3NQvncXBY4VtvSju4N3WOs17l6cXHxoblGwKRo + r8mlHeAtHZO2b9mfDohem8sCR2XX3K4z0hj65NCYklSTGONpuUzApNlVFFWaqP81NHF7mvD2tnM7lwbu + V/vwqTR2vrV+LPU7VSjf4ff+MAU0B65NeWVd12fm0sBhpTnzosHDp7rGUV8T9lVFvDiXCJgSrmEezDea + onhMrgsM25rmyuvSODkwNG56nvZyYvi5XCNg2mgOXM3tVVH9ci4L3KtpmlN9W7Y+VYhfiEkuEzCt8n3L + fzA8yXuY/b7OZEVZlovt3ew6xknf8965ublTcpmAaac5cG3C2zQ09Vv7bVAaC/+7fmz0Og6QYVZpDlyT + GD/dPlgpl4b+2Nru5NIYuGfdmOhxmhB/VBblL+QaATNKc+DBfH1nCI/OdWHGtTewSdv874fGgIT4xfYb + wlwmYNZpDlzNbVUIz85lYUblJ2i6BDacGP/u7O3bH5zLBPSF5sDV7K+L+Nu5LMyYtJP7xbSNfzy0zfue + A+03gak8WwdVAnpHc+CaxHiJ5sCZsnK9f/+6bd3v3JZ2/r+SawT0mebAg0kfjB93v/Pp136t3X693bWN + e56v6nsBhmkOXE24Ph0EnJXrwpSp63qhDuXnu7dtn1P+U1VVP5PLBHAozYGDtD+LchvU6TN4Iqa+lqGs + XO8/YVAlgMPQHLiSsC+dNb0ml4UJVxflRWm73b1+O/Y5YXcVwvNziQDun+bANYnxkvO2bHlALg0TJsZ4 + cl3ESzu3Xa8Trm+KYimXCeDIaQ48mKqIH9McOHl2zc/vaIr42a5t1vN8tCiK03OZAI6J5sCVFPFr7QNk + cl0Ys3yp6nvrtlO/s3K9f9ugSgDHSXPgILk58PxcFsYkX+93J8s1qUK8oynKF+YSAYyO5sCVhH3pgOjV + uSxsoqZpHpjq//bu7dLjFPHGND+Xc5kARk9z4JrE+JZUEl+1bpLFhYW5VPf/WLcd5N/ruj4zlwlg42gO + PCQfdXOVjdeE8MRU6xuGai9uXw2MgebA1YTrFkMoc10YsaYoX5rqfOf6uvc6e9LO/xW5RACbT3Pgam5N + B0Q/m8vCCLT3XnCQ2ZXwnWqhenIuE8D4aA5czV3OykZj19yuM1I9PzlUXwnhirIst+cyAYyf5sA1GTQH + uu/6MdoZ4+NTHb+1rq59j+v9wKTSHHhIPtI0zam5NByhNH5enGr306Fa9j1720ttuUQAE0tz4Epi/FJM + cl24b8ZNd25KdTk31whg8mkOXEm4pX1EbS4LHebn5x+WdnIf765fr3NVCOFRuUwA00Nz4Gr21kX58lwW + 1qjmq8el+nxjqF4S4yVLS0sn5TIBTB/NgWuiOfAQTVE+J9XltnV16nXCvqqIF+cSAUw3zYGH5INnb9/+ + 4Fyavtra7uRSLe4Zqk3f88MmxvNyjQBmhiavg/liVVVFrkuvLC4uPjSt/4eG6iEhXlOWZchlApg9mgNX + c3P6wD8nl6UXqvlqZ1rv/xmqQ+/ThPJv5ufnH5TLBDC7NAeuZm97n/tclplWhfCstL4/GVr/nsf1fqCH + NAeu5kB7aSSVZFabA13v786tVVFckGsE0C+aAw8mnSG/f25u7pRcmpnQbt8mlB/oWt8+pwnxWk+PBNAc + uJoqxC/MSnNgs7BQ1yFc17WePc97Z+1AD+C4aA5czU3T/qjXdED3zLQePx5ar75nf77ev3VQJQBWaQ5c + zZ4qhJfkskyVuigvapvbOtapt2lC/FFTls/IJQKgi+bA1aw0B07FGWOM8eQ6xnd1rEe/E+OX2jGdywTA + fdEcuCYx/u2kXzNudjTz6az/c53L3+NUMX7YI6EBjp7mwIO5pt3J5rpMlHKhfFpavu8PLW/fM+s/7QTY + eJoDV/PduiiWc1kmwuB6f7x7aDn7ntvrGC/MJQLgeGgOHKQK8Y46hOflsoxN0zQPbEJ8Z9cy9jxf3RnC + o3OZABgFzYGrab9efl0qyViaAxcXFubSgchnOpar77k8xnhaLhMAo6Q5cE2K+L7NfoBMHcJT0nvfvG5Z + +h3X+wE2iebAg7m6LMvtuS4bKl/v14txSMLuKoQX5BIBsBk0B64kfCfV4om5LCN33pYtD3DA1ZVwfb1Q + PzaXCYDNpDlwJWH3RnSe75rbdUZ6/X9b/369z0eLojg9lwmAcdAcuJqV5sCRaEJ4QnrNbw+9R9+zcr1/ + 26BKAIyV5sBDcll7W95cmmPSPocgvc6dQ6/b9+xpivJluUQATBDNgQdz1TE2B6phV4p446TdhAmAIens + 9ZXpQ1tz4KA58Am5LPfrrB07Hp7+zr90v1Z/UxXxU3Vdn5nLBMAk0xy4krC7WSifm8tyWGVZnl2F+M3u + 1+hxYrxkeXn5xFwmAKaB5sDV7E9nsRfnsqyTdnIvGtxiuPPv9jV7Ul1ekUsEwLTRHHgwVSjfsbS0dFIu + TWtr+6uB9P8ODP/Znue7ZVmek2sEwBTT2Laa8sr2enb7jHoHRl0JV8QYH5nHDQCzwJ0DV/P1tKO7vuO/ + 9zpVKN/qej/AjNIcKB3Z24TyVXmIADCrNAfKmtzUxHhuHhoAzDrNgZJyVQjhUXlIANAjmgN7m/Du471d + MgBTzp0D+5Sw777uiQBAz2gO7EPCLSnn500OAAOaA2c615RlGfKmBoBDaQ6cvTQhvmd+fv5BeRMDwGFp + DpyJuN4PwDHQHDjVubUqigvypgSAo6M5cPrShHjtYghl3oQAcGw0B05Rivi+ubm5U/KmA4Djozlw4rM/ + X+/fOthiADA6mgMnME2IP2rK8hl5GwHAxtAcOFH5SozxrLxpAGBjaQ4cf6oYP9w0zal5kwDA5tAcOLYc + aC/FpE1wwmBLAMAm0xy46bk91fvCXH4AGCvNgZuRIn6tKYrH5JoDwGTQHLihuTzGeFouNQBMFs2BI4/r + /QBMB82Bo0rYXYXwglxWAJh8mgOPN+H6eqF+bC4nAEwVzYHHkiL+c1EUp+caAsB00hx4FInxLalk2waV + A4AppznwfrOnLsqX53IBwOzQHHiYFPHGaqF6Ui4TAMwezYGHpirip+q6PjOXBwBmmubANjFesry8fGKu + CQD0Q4+bA/dWMf56LgMA9E8PmwO/W5blOXn1AaC/+tMcWF4ZY3xkXm0AYOabA2O8ZGlp6aS8ugDAGrPY + HLi3CeWr8voBAIczQ82BN6UDmnPzagEA92f6mwPLz1dVVeTVAQCO1LQ2B1Yh/PX8/PyD8moAAEdrupoD + w76qiBfnRQcAjtMUNAeGW1LOz8sLAIzKBDcHXlOWZciLCQCM2gQ2B142Nzd3Sl48AGCjTEhz4H7X+wFg + k425OfDWqqh+Pi8KALDJtqWDgDemHfKBoR30hqUJ8dqY5PcHAMalKcrnpJ3z94Z31qNO+/t+1/sBYIKk + k/LT6hD+Mu2oR/4rgXTW/+X02r+U3woAmDTtz/GaIv5F2nH/ZHhHfpS5J+Vf01n/S9LLbhu8OgAw0dpb + 8TYL5XPTmfvb0o78v/MOvWtHvybtzXzKT1Qx/n5d1wv5pQCAaXXvAUFRLLXd+3WMFzZF+cKUl7X/rIri + gsWFhbn8RwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6LEtW/4flgYiLD1qeX0A + AAAASUVORK5CYII= + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + xAAADsQBlSsOGwAAAIZJREFUOE+1j10KwCAMgz2b755xl/IsvnaL2K20UfbDAmEako+ZROSTafjE12Go + tbbB43rK5xSAQq1VYFtmeQBoqZTSreVZvgTknM8yyyjA/qodsDF9gspD2Bj6B+DH+NqzhQQAG+POMnSX + AFuc5QFgn6ClHh5iOQVAKNixyucB8NY0vG9JOzzyhrdq5IRgAAAAAElFTkSuQmCC + + + \ No newline at end of file diff --git a/SCrawler/API/Mastodon/SettingsForm.vb b/SCrawler/API/Mastodon/SettingsForm.vb new file mode 100644 index 0000000..dbd329c --- /dev/null +++ b/SCrawler/API/Mastodon/SettingsForm.vb @@ -0,0 +1,154 @@ +' 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.Controls.Base +Namespace API.Mastodon + Friend Class SettingsForm + Private WithEvents MyDefs As DefaultFormOptions + Friend ReadOnly Property MyCredentials As List(Of Credentials) + Friend ReadOnly Property MyDomains As List(Of String) + Friend Sub New(ByVal s As SiteSettings) + InitializeComponent() + MyCredentials = New List(Of Credentials) + If s.Domains.Credentials.Count > 0 Then MyCredentials.AddRange(s.Domains.Credentials) + MyDomains = New List(Of String) + MyDomains.ListAddList(s.Domains) + MyDefs = New DefaultFormOptions(Me, Settings.Design) + End Sub + Private Sub MyForm_Load(sender As Object, e As EventArgs) Handles Me.Load + With MyDefs + .MyView = New FormView(Me, Settings.Design, "MastodonSettingsForm") + .MyView.Import() + .MyView.SetFormSize() + .AddOkCancelToolbar() + RefillList() + .EndLoaderOperations() + End With + End Sub + Private Sub SettingsForm_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed + MyCredentials.Clear() + MyDomains.Clear() + End Sub + Private Sub RefillList() + CMB_DOMAINS.Items.Clear() + If MyDomains.Count > 0 Then + MyDomains.Sort() + CMB_DOMAINS.BeginUpdate() + CMB_DOMAINS.Items.AddRange(MyDomains.Select(Function(d) New ListItem(d))) + CMB_DOMAINS.EndUpdate(True) + End If + End Sub + Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick + ApplyCredentials() + If MyCredentials.Count > 0 Then MyCredentials.RemoveAll(Function(c) c.Domain.IsEmptyString Or c.Bearer.IsEmptyString Or c.Csrf.IsEmptyString) + If MyDomains.Count > 0 Then + If MyCredentials.Count > 0 Then + MyCredentials.RemoveAll(Function(c) Not MyDomains.Contains(c.Domain)) + Else + MyCredentials.Clear() + End If + End If + MyDefs.CloseForm() + End Sub + Private Sub CMB_DOMAINS_ActionOnButtonClick(ByVal Sender As ActionButton, ByVal e As ActionButtonEventArgs) Handles CMB_DOMAINS.ActionOnButtonClick + Try + Dim d$ + Dim i% = -1 + Select Case e.DefaultButton + Case ActionButton.DefaultButtons.Add + d = InputBoxE("Enter a new domain using the pattern [mastodon.social]:", "New domain") + If Not d.IsEmptyString Then + If MyDomains.Count > 0 Then i = MyDomains.IndexOf(d) + If i >= 0 Then + MsgBoxE({$"Domain '{d}' already exists", "Add domain"}, vbExclamation) + If i <= CMB_DOMAINS.Count - 1 Then CMB_DOMAINS.SelectedIndex = i + Else + ApplyCredentials() + ClearCredentials() + MyDomains.Add(d) + _Suspended = True + RefillList() + _Suspended = False + i = MyDomains.IndexOf(d) + If i.ValueBetween(0, CMB_DOMAINS.Count - 1) Then + CMB_DOMAINS.SelectedIndex = i + Else + _LatestSelected = -1 + _CurrentCredentialsIndex = -1 + _CurrentDomain = String.Empty + End If + End If + End If + Case ActionButton.DefaultButtons.Delete + If _LatestSelected >= 0 Then + d = CMB_DOMAINS.Items(_LatestSelected).Value(0) + If Not d.IsEmptyString AndAlso MsgBoxE({$"Are you sure you want to delete the [{d}] domain?", + "Removing domains"}, vbYesNo) = vbYes Then + i = MyDomains.IndexOf(d) + Dim l% = _LatestSelected + If i >= 0 Then + ClearCredentials() + MyDomains.RemoveAt(i) + _Suspended = True + RefillList() + _Suspended = False + If (l - 1).ValueBetween(0, CMB_DOMAINS.Count - 1) Then + CMB_DOMAINS.SelectedIndex = l - 1 + Else + _LatestSelected = -1 + _CurrentCredentialsIndex = -1 + _CurrentDomain = String.Empty + End If + End If + End If + End If + End Select + Catch ex As Exception + ErrorsDescriber.Execute(EDP.LogMessageValue, ex, "API.Mastodon.SettingsForm.ActionButtonClick") + End Try + End Sub + Private _LatestSelected As Integer = -1 + Private _CurrentCredentialsIndex As Integer = -1 + Private _CurrentDomain As String = String.Empty + Private _Suspended As Boolean = False + Private Sub CMB_DOMAINS_ActionSelectedItemChanged(ByVal Sender As Object, ByVal e As EventArgs, ByVal Item As ListViewItem) Handles CMB_DOMAINS.ActionSelectedItemChanged + If Not MyDefs.Initializing And Not _Suspended Then + Dim DropCredentials As Boolean = True + If Not Item Is Nothing Then + ApplyCredentials() + _LatestSelected = Item.Index + _CurrentDomain = Item.Text + If MyCredentials.Count > 0 And Not _CurrentDomain.IsEmptyString Then + _CurrentCredentialsIndex = MyCredentials.IndexOf(_CurrentDomain) + If _CurrentCredentialsIndex >= 0 Then + With MyCredentials(_CurrentCredentialsIndex) : TXT_AUTH.Text = .Bearer : TXT_TOKEN.Text = .Csrf : End With + DropCredentials = False + End If + Else + _CurrentCredentialsIndex = -1 + End If + End If + If DropCredentials Then ClearCredentials() + End If + End Sub + Private Sub ClearCredentials() + TXT_AUTH.Clear() + TXT_TOKEN.Clear() + End Sub + Private Sub ApplyCredentials() + Try + If _LatestSelected >= 0 And Not _CurrentDomain.IsEmptyString Then + Dim c As New Credentials With {.Domain = _CurrentDomain, .Bearer = TXT_AUTH.Text, .Csrf = TXT_TOKEN.Text} + If _CurrentCredentialsIndex.ValueBetween(0, MyCredentials.Count - 1) Then MyCredentials(_CurrentCredentialsIndex) = c Else MyCredentials.Add(c) + End If + Catch ex As Exception + End Try + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Mastodon/SiteSettings.vb b/SCrawler/API/Mastodon/SiteSettings.vb new file mode 100644 index 0000000..4cd056e --- /dev/null +++ b/SCrawler/API/Mastodon/SiteSettings.vb @@ -0,0 +1,214 @@ +' 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.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Documents.JSON +Imports TS = SCrawler.API.Twitter.SiteSettings +Namespace API.Mastodon + + Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon + Get + Return My.Resources.SiteResources.MastodonIcon_48 + End Get + End Property + Friend Overrides ReadOnly Property Image As Image + Get + Return My.Resources.SiteResources.MastodonPic_48 + End Get + End Property +#Region "Domains" + Private ReadOnly Property SiteDomains As PropertyValue + Friend ReadOnly Property Domains As MastodonDomains + Private ReadOnly Property DomainsLastUpdateDate As PropertyValue +#End Region +#Region "Auth" + + Friend ReadOnly Property MyDomain As PropertyValue + + Friend ReadOnly Property Auth As PropertyValue + + Friend ReadOnly Property Token As PropertyValue + Private Sub ChangeResponserFields(ByVal PropName As String, ByVal Value As Object) + If Not PropName.IsEmptyString Then + Dim f$ = String.Empty + Select Case PropName + Case NameOf(Auth) : f = TS.Header_Authorization + Case NameOf(Token) : f = TS.Header_Token + End Select + If Not f.IsEmptyString Then + Responser.Headers.Remove(f) + If Not CStr(Value).IsEmptyString Then Responser.Headers.Add(f, CStr(Value)) + Responser.SaveSettings() + End If + End If + End Sub +#End Region +#Region "Other properties" + + Friend ReadOnly Property GifsDownload As PropertyValue + + Friend ReadOnly Property GifsSpecialFolder As PropertyValue + + Friend ReadOnly Property GifsPrefix As PropertyValue + + Private ReadOnly Property GifStringChecker As IFormatProvider + + Friend ReadOnly Property UseMD5Comparison As PropertyValue + + Friend ReadOnly Property UserRelatedToMyDomain As PropertyValue +#End Region +#End Region +#Region "Initializer" + Friend Sub New() + MyBase.New("Mastodon", "mastodon.social") + + Domains = New MastodonDomains(Me, "mastodon.social") + SiteDomains = New PropertyValue(Domains.DomainsDefault, GetType(String)) + Domains.DestinationProp = SiteDomains + DomainsLastUpdateDate = New PropertyValue(Now.AddYears(-1)) + + Auth = New PropertyValue(Responser.Headers.Value(TS.Header_Authorization), GetType(String), Sub(v) ChangeResponserFields(NameOf(Auth), v)) + Token = New PropertyValue(Responser.Headers.Value(TS.Header_Token), GetType(String), Sub(v) ChangeResponserFields(NameOf(Token), v)) + + GifsDownload = New PropertyValue(True) + GifsSpecialFolder = New PropertyValue(String.Empty, GetType(String)) + GifsPrefix = New PropertyValue("GIF_") + GifStringChecker = New TS.GifStringProvider + UseMD5Comparison = New PropertyValue(False) + MyDomain = New PropertyValue(String.Empty, GetType(String)) + UserRelatedToMyDomain = New PropertyValue(False) + + UserRegex = RParams.DMS("", 0, RegexReturn.ListByMatch, EDP.ReturnValue) + End Sub + Friend Overrides Sub EndInit() + Domains.PopulateInitialDomains(SiteDomains.Value) + If CDate(DomainsLastUpdateDate.Value).AddDays(7) < Now Then UpdateServersList() + MyBase.EndInit() + End Sub +#End Region +#Region "GetInstance" + Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider + Return New UserData + End Function +#End Region +#Region "Domains Support" + Protected Overrides Sub DomainsApply() + Domains.Apply() + MyBase.DomainsApply() + End Sub + Protected Overrides Sub DomainsReset() + Domains.Reset() + MyBase.DomainsReset() + End Sub + Friend Overrides Sub OpenSettingsForm() + Domains.OpenSettingsForm() + End Sub +#End Region +#Region "Update" + Friend Overrides Sub Update() + If _SiteEditorFormOpened Then + Dim tf$ = GifsSpecialFolder.Value + If Not tf.IsEmptyString Then tf = tf.StringTrim("\") : GifsSpecialFolder.Value = tf + End If + MyBase.Update() + End Sub +#End Region +#Region "UserOptions" + Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) + If Options Is Nothing OrElse (Not TypeOf Options Is Twitter.EditorExchangeOptions OrElse + Not DirectCast(Options, Twitter.EditorExchangeOptions).SiteKey = MastodonSiteKey) Then _ + Options = New Twitter.EditorExchangeOptions(Me) With {.SiteKey = MastodonSiteKey} + If OpenForm Then + Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using + End If + End Sub +#End Region +#Region "Download" + Friend Overrides Function BaseAuthExists() As Boolean + Return ACheck(Token.Value) And ACheck(Auth.Value) And ACheck(MyDomain.Value) + End Function + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + If What = ISiteSettings.Download.SavedPosts Or What = ISiteSettings.Download.SingleObject Then + If Not ACheck(MyDomain.Value) Then MyMainLOG = "Mastodon account domain not set" : Return False + Else + If CDate(DomainsLastUpdateDate.Value).AddDays(7) < Now Then UpdateServersList() + End If + Return MyBase.Available(What, Silent) + End Function +#End Region +#Region "GetUserUrl, GetUserPostUrl" + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String + If Not ACheck(MyDomain.Value) Then Return String.Empty + With DirectCast(User, UserData) + If UserRelatedToMyDomain.Value Then + If MyDomain.Value = .UserDomain Then + Return $"https://{ .UserDomain}/@{ .TrueName}" + Else + Return $"https://{MyDomain.Value}/@{ .TrueName}@{ .UserDomain}" + End If + Else + Return $"https://{ .UserDomain}/@{ .TrueName}" + End If + End With + End Function + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + If Not ACheck(MyDomain.Value) Then Return String.Empty + Return $"{GetUserUrl(User)}/{Media.Post.ID}" + End Function +#End Region +#Region "IsMyUser, IsMyImageVideo" + Private Const UserRegexDefault As String = "https?://{0}/@([^/@]+)@?([^/]*)" + Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions + If Domains.Count > 0 Then + Dim l As List(Of String) + For Each domain$ In Domains + UserRegex.Pattern = String.Format(UserRegexDefault, domain) + l = RegexReplace(UserURL, UserRegex) + If l.ListExists(2) Then Return New ExchangeOptions(Site, $"{l(2).IfNullOrEmpty(domain)}@{l(1)}") + Next + End If + Return Nothing + End Function + Friend Overrides Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions + If Not URL.IsEmptyString And Domains.Count > 0 Then + If Domains.Domains.Exists(Function(d) URL.Contains(d)) Then Return New ExchangeOptions(Site, URL) With {.Exists = True} + End If + Return Nothing + End Function +#End Region +#Region "UpdateServersList" + Private Sub UpdateServersList() + Try + Dim r$ = GetWebString("https://api.joinmastodon.org/servers?language=&category=®ion=&ownership=®istrations=",, EDP.ThrowException) + If Not r.IsEmptyString Then + Dim j As EContainer = JsonDocument.Parse(r, EDP.ReturnValue) + If If(j?.Count, 0) > 0 Then + Domains.Domains.ListAddList(j.Select(Function(e) e.Value("domain")), LAP.NotContainsOnly, EDP.ReturnValue) + Domains.Domains.Sort() + Domains.Save() + j.Dispose() + End If + End If + DomainsLastUpdateDate.Value = Now + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, "[API.Mastodon.SiteSettings.UpdateServersList]") + End Try + End Sub +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Mastodon/UserData.vb b/SCrawler/API/Mastodon/UserData.vb new file mode 100644 index 0000000..3c7a4ad --- /dev/null +++ b/SCrawler/API/Mastodon/UserData.vb @@ -0,0 +1,287 @@ +' Copyright (C) 2023 Andy https://github.com/AAndyProgram +' This program is free software: you can redistribute it and/or modify +' it under the terms of the GNU General Public License as published by +' the Free Software Foundation, either version 3 of the License, or +' (at your option) any later version. +' +' This program is distributed in the hope that it will be useful, +' but WITHOUT ANY WARRANTY +Imports System.Threading +Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Documents.JSON +Imports UTypes = SCrawler.API.Base.UserMedia.Types +Namespace API.Mastodon + Friend Class UserData : Inherits Twitter.UserData +#Region "XML names" + Private Const Name_UserDomain As String = "UserDomain" + Private Const Name_TrueName As String = "TrueName" +#End Region +#Region "Declarations" + Private _UserDomain As String = String.Empty + Friend Property UserDomain As String + Get + Return _UserDomain.IfNullOrEmpty(MySettings.MyDomain.Value) + End Get + Set(ByVal d As String) + _UserDomain = d + End Set + End Property + Friend Property TrueName As String = String.Empty + Private ReadOnly Property MySettings As SiteSettings + Get + Return HOST.Source + End Get + End Property + Private MyCredentials As Credentials + Private Sub ResetCredentials() + MyCredentials = Nothing + With MySettings + Dim setDef As Boolean = True + If Not IsSavedPosts Then + If ACheck(.MyDomain.Value) AndAlso UserDomain = .MyDomain.Value Then + setDef = True + ElseIf .Domains.Credentials.Count > 0 Then + Dim i% = .Domains.Credentials.IndexOf(UserDomain) + If i >= 0 Then + MyCredentials = .Domains.Credentials(i) + setDef = Not MyCredentials.Exists + End If + End If + End If + If setDef Then MyCredentials = New Credentials With {.Domain = UserDomain, .Bearer = MySettings.Auth.Value, .Csrf = MySettings.Token.Value} + End With + With MyCredentials + Responser.Headers.Add(Twitter.SiteSettings.Header_Authorization, .Bearer) + Responser.Headers.Add(Twitter.SiteSettings.Header_Token, .Csrf) + End With + End Sub +#End Region +#Region "LoadUserInformation" + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + MyBase.LoadUserInformation_OptionalFields(Container, Loading) + Dim obtainNames As Action = Sub() + If _UserDomain.IsEmptyString And Not Name.IsEmptyString Then + Dim l$() = Name.Split("@") + If l.ListExists(2) Then + _UserDomain = l(0) + TrueName = l(1) + Else + _UserDomain = MySettings.MyDomain.Value + TrueName = Name + End If + If FriendlyName.IsEmptyString Then FriendlyName = TrueName + End If + End Sub + If Loading Then + _UserDomain = Container.Value(Name_UserDomain) + TrueName = Container.Value(Name_TrueName) + obtainNames.Invoke + Else + obtainNames.Invoke + Container.Add(Name_UserDomain, _UserDomain) + Container.Add(Name_TrueName, TrueName) + Container.Value(Name_FriendlyName) = FriendlyName + End If + End Sub +#End Region +#Region "Download functions" + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + ResetCredentials() + DownloadData(String.Empty, Token) + End Sub + Private Overloads Sub DownloadData(ByVal POST As String, ByVal Token As CancellationToken) + Dim URL$ = String.Empty + Try + Dim PostID$ = String.Empty + Dim PostDate$ + Dim s As EContainer, ss As EContainer + Dim NewPostDetected As Boolean = False + Dim ExistsDetected As Boolean = False + + If IsSavedPosts Then + URL = $"https://{MySettings.MyDomain.Value}/api/v1/bookmarks" + If Not POST.IsEmptyString Then URL &= $"?max_id={POST}" + Else + If POST.IsEmptyString And ID.IsEmptyString Then + ObtainUserID() + If ID.IsEmptyString Then Throw New ArgumentNullException("ID", "Unable to get user ID") With {.HelpLink = 1} + End If + URL = $"https://{MyCredentials.Domain}/api/v1/accounts/{ID}/statuses?" + If ParseUserMediaOnly Then URL &= "only_media=true&" + URL &= "limit=40" + If Not POST.IsEmptyString Then URL &= $"&max_id={POST}" + End If + + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + Using j As EContainer = JsonDocument.Parse(r) + If If(j?.Count, 0) > 0 Then + For Each jj As EContainer In j + With jj + If Not IsSavedPosts And POST.IsEmptyString And Not .Item("account") Is Nothing Then + With .Item("account") + If .Value("id") = ID Then + UserSiteNameUpdate(.Value("display_name")) + UserDescriptionUpdate(.Value("note")) + Dim __getImage As Action(Of String) = Sub(ByVal img As String) + If Not img.IsEmptyString Then + Dim __imgFile As SFile = img + If Not __imgFile.Name.IsEmptyString Then + If __imgFile.Extension.IsEmptyString Then __imgFile.Extension = "jpg" + __imgFile.Path = MyFile.CutPath.Path + If Not __imgFile.Exists Then GetWebFile(img, __imgFile, EDP.None) + End If + End If + End Sub + __getImage.Invoke(.Value("header").IfNullOrEmpty(.Value("header_static"))) + __getImage.Invoke(.Value("avatar").IfNullOrEmpty(.Value("avatar_static"))) + End If + End With + End If + + PostID = .Value("id") + PostDate = .Value("created_at") + + If Not IsSavedPosts And Not PostDate.IsEmptyString Then + Select Case CheckDatesLimit(PostDate, DateProvider) + Case DateResult.Skip : Continue For + Case DateResult.Exit : Exit Sub + End Select + End If + + If Not _TempPostsList.Contains(PostID) Then + NewPostDetected = True + _TempPostsList.Add(PostID) + Else + ExistsDetected = True + Continue For + End If + + If IsSavedPosts OrElse (Not ParseUserMediaOnly OrElse + (If(.Item("reblog")?.Count, 0) = 0 OrElse .Value({"reblog", "account"}, "id") = ID)) Then + If If(.Item("media_attachments")?.Count, 0) > 0 Then + s = .Item("media_attachments") + Else + s = .Item({"reblog", "account"}, "media_attachments") + End If + If s.ListExists Then + For Each ss In s : ObtainMedia(ss, PostID, PostDate) : Next + End If + End If + End With + Next + End If + End Using + End If + + If POST.IsEmptyString And ExistsDetected Then Exit Sub + If Not PostID.IsEmptyString And NewPostDetected Then DownloadData(PostID, Token) + Catch ex As Exception + ProcessException(ex, Token, $"data downloading error{IIf(IsSavedPosts, " (Saved Posts)", String.Empty)} [{URL}]") + End Try + End Sub + Private Sub ObtainMedia(ByVal e As EContainer, ByVal PostID As String, ByVal PostDate As String, Optional ByVal BaseUrl As String = Nothing) + Dim t As UTypes = UTypes.Undefined + Select Case e.Value("type") + Case "video" : t = UTypes.Video + Case "image" : t = UTypes.Picture + Case "gifv" : t = UTypes.GIF + End Select + If Not t = UTypes.Undefined Then + Dim m As New UserMedia(e.Value("url"), t) With { + .Post = New UserPost(PostID, AConvert(Of Date)(PostDate, DateProvider, Nothing, EDP.ReturnValue)), + .URL_BASE = BaseUrl.IfNullOrEmpty(MySettings.GetUserPostUrl(Me, m)) + } + If Not t = UTypes.GIF Or GifsDownload Then + If t = UTypes.GIF Then + If Not GifsSpecialFolder.IsEmptyString Then m.SpecialFolder = GifsSpecialFolder + If Not GifsPrefix.IsEmptyString Then m.File.Name = $"{GifsPrefix}{m.File.Name}" + End If + If Not m.URL.IsEmptyString Then _TempMediaList.ListAddValue(m, LNC) + End If + End If + End Sub + Private Sub ObtainUserID() + Try + If ID.IsEmptyString Then + Dim url$ = $"https://{MyCredentials.Domain}/api/v1/accounts/lookup?acct=" + If Not UserDomain.IsEmptyString Then + If UserDomain = MyCredentials.Domain Then + url &= $"@{TrueName}" + Else + url &= $"@{TrueName}@{UserDomain}" + End If + Else + url &= $"@{TrueName}" + End If + Dim r$ = Responser.GetResponse(url) + If Not r.IsEmptyString Then + Using j As EContainer = JsonDocument.Parse(r) + If Not j Is Nothing Then ID = j.Value("id") + End Using + End If + End If + Catch ex As Exception + ErrorsDescriber.Execute(EDP.SendToLog, ex, $"API.Mastodon.UserData.ObtainUserID({ToStringForLog()})") + End Try + End Sub + Private Function GetSinglePostPattern(ByVal Domain As String) As String + Return $"https://{Domain}/api/v1/statuses/" & "{0}" + End Function + Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken) + SinglePostUrl = GetSinglePostPattern(MyCredentials.Domain) + MyBase.ReparseMissing(Token) + End Sub +#End Region +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim PostID$ = RegexReplace(Data.URL, RParams.DM("(?<=/)\d+", 0, EDP.ReturnValue)) + If Not PostID.IsEmptyString Then + ResetCredentials() + Dim pattern$ + If Not ACheck(MySettings.MyDomain.Value) Then + Throw New ArgumentNullException("Mastodon domain", "Mastodon domain not set") + Else + pattern = GetSinglePostPattern(MySettings.MyDomain.Value) + End If + Dim r$ = Responser.GetResponse(String.Format(pattern, PostID),, EDP.ReturnValue) + If Not r.IsEmptyString Then + Using j As EContainer = JsonDocument.Parse(r) + If j.ListExists AndAlso j.Contains("media_attachments") Then + For Each jj As EContainer In j("media_attachments") : ObtainMedia(jj, PostID, String.Empty, Data.URL) : Next + End If + End Using + End If + End If + End Sub +#End Region +#Region "Exception" + 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 TypeOf ex Is ArgumentNullException AndAlso Not ex.HelpLink.IsEmptyString And ex.HelpLink = 1 Then + Return 0 + Else + If Responser.Status = Net.WebExceptionStatus.NameResolutionFailure Then + MyMainLOG = $"User domain ({UserDomain}) not found: {ToStringForLog()}" + Return 1 + ElseIf Responser.StatusCode = Net.HttpStatusCode.NotFound Then + UserExists = False + Return 1 + ElseIf Responser.StatusCode = Net.HttpStatusCode.Unauthorized Then + MyMainLOG = $"{ToStringForLog()}: account credentials have expired" + Return 2 + ElseIf Responser.StatusCode = Net.HttpStatusCode.Gone Then + UserSuspended = True + Return 1 + Else + Return 0 + End If + End If + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PathPlugin/SiteSettings.vb b/SCrawler/API/PathPlugin/SiteSettings.vb index ad6c15d..0387608 100644 --- a/SCrawler/API/PathPlugin/SiteSettings.vb +++ b/SCrawler/API/PathPlugin/SiteSettings.vb @@ -12,9 +12,10 @@ Imports SCrawler.Plugin.Attributes Namespace API.PathPlugin Friend Class SiteSettings : Inherits SiteSettingsBase + Private ReadOnly _Icon As Icon = Nothing Friend Overrides ReadOnly Property Icon As Icon Get - Return PersonalUtilities.Tools.ImageRenderer.GetIcon(PersonalUtilities.My.Resources.FolderOpenPic_Orange_16, EDP.ReturnValue) + Return _Icon End Get End Property Friend Overrides ReadOnly Property Image As Image @@ -24,6 +25,7 @@ Namespace API.PathPlugin End Property Friend Sub New() MyBase.New(PluginName) + _Icon = PersonalUtilities.Tools.ImageRenderer.GetIcon(PersonalUtilities.My.Resources.FolderOpenPic_Orange_16, EDP.ReturnValue) End Sub Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider Return New UserData @@ -42,7 +44,7 @@ Namespace API.PathPlugin Friend Overrides Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions Return Nothing End Function - Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider, ByVal Channel As Boolean) As String + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String Return String.Empty End Function End Class diff --git a/SCrawler/API/Pinterest/Declarations.vb b/SCrawler/API/Pinterest/Declarations.vb new file mode 100644 index 0000000..faf2bc9 --- /dev/null +++ b/SCrawler/API/Pinterest/Declarations.vb @@ -0,0 +1,21 @@ +' 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 +Namespace API.Pinterest + Friend Module Declarations + Friend ReadOnly DateProvider As ADateTime = GetDateProvider() + Private Function GetDateProvider() As ADateTime + Dim n As DateTimeFormatInfo = CultureInfo.GetCultureInfo("en-us").DateTimeFormat.Clone + n.FullDateTimePattern = "ddd dd MMM yyyy HH:mm:ss" + n.TimeSeparator = String.Empty + 'Sat, 01 Jan 2000 01:10:15 +0000 + 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/Pinterest/SiteSettings.vb b/SCrawler/API/Pinterest/SiteSettings.vb new file mode 100644 index 0000000..7eaedd7 --- /dev/null +++ b/SCrawler/API/Pinterest/SiteSettings.vb @@ -0,0 +1,101 @@ +' 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.Forms +Imports PersonalUtilities.Functions.RegularExpressions +Namespace API.Pinterest + + Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon + Get + Return My.Resources.SiteResources.PinterestIcon_32 + End Get + End Property + Friend Overrides ReadOnly Property Image As Image + Get + Return My.Resources.SiteResources.PinterestPic_48 + End Get + End Property + Private Class ConcurrentDownloadsValidator : Inherits FieldsCheckerProviderBase + Public Overrides Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object + Dim v% = AConvert(Of Integer)(Value, -1) + Dim defV% = Settings.MaxUsersJobsCount + If v.ValueBetween(1, defV) Then + Return Value + Else + ErrorMessage = $"The number of concurrent downloads must be greater than 0 and equal to or less than {defV} (global limit)." + HasError = True + Return Nothing + End If + End Function + End Class + + Private ReadOnly Property ConcurrentDownloadsProvider As IFormatProvider + + Friend ReadOnly Property ConcurrentDownloads As PropertyValue + + Friend ReadOnly Property SavedPostsUserName As PropertyValue +#End Region +#Region "Initializer" + Friend Sub New() + MyBase.New("Pinterest", "pinterest.com") + SavedPostsUserName = New PropertyValue(String.Empty, GetType(String)) + ConcurrentDownloads = New PropertyValue(1) + ConcurrentDownloadsProvider = New ConcurrentDownloadsValidator + CheckNetscapeCookiesOnEndInit = True + UseNetscapeCookies = True + UserRegex = RParams.DMS("https?://w{0,3}.?[^/]*?.?pinterest.com/([^/]+)/?(?(_)|([^/]*))", 0, RegexReturn.ListByMatch, EDP.ReturnValue) + End Sub +#End Region +#Region "GetInstance, Available" + Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider + Return New UserData + End Function + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + Return Settings.GalleryDLFile.Exists And (Not What = ISiteSettings.Download.SavedPosts OrElse + (Responser.CookiesExists And ACheck(SavedPostsUserName.Value))) + End Function +#End Region +#Region "IsMyUser, IsMyImageVideo, GetUserUrl, GetUserPostUrl" + Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions + If Not UserURL.IsEmptyString Then + Dim l As List(Of String) = RegexReplace(UserURL, UserRegex) + If l.ListExists(3) Then + Dim n$ = l(1) + If Not l(2).IsEmptyString Then n &= $"@{l(2)}" + Return New ExchangeOptions(Site, n) With {.Exists = True} + End If + End If + Return Nothing + End Function + Friend Overrides Function IsMyImageVideo(ByVal URL As String) As ExchangeOptions + Return IsMyUser(URL) + End Function + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String + With DirectCast(User, UserData) + Dim n$ = .TrueUserName + Dim c$ = .TrueBoardName + If Not c.IsEmptyString Then c &= "/" + Return $"https://www.pinterest.com/{n}/{c}" + End With + End Function + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + If Not Media.Post.ID.IsEmptyString Then + Return $"https://www.pinterest.com/pin/{Media.Post.ID}/" + Else + Return String.Empty + End If + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/Pinterest/UserData.vb b/SCrawler/API/Pinterest/UserData.vb new file mode 100644 index 0000000..c46b2fe --- /dev/null +++ b/SCrawler/API/Pinterest/UserData.vb @@ -0,0 +1,330 @@ +' Copyright (C) 2023 Andy https://github.com/AAndyProgram +' This program is free software: you can redistribute it and/or modify +' it under the terms of the GNU General Public License as published by +' the Free Software Foundation, either version 3 of the License, or +' (at your option) any later version. +' +' This program is distributed in the hope that it will be useful, +' but WITHOUT ANY WARRANTY +Imports System.Threading +Imports SCrawler.API.Base +Imports SCrawler.API.Base.GDL +Imports SCrawler.API.YouTube.Objects +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Tools.Web.Documents.JSON +Namespace API.Pinterest + Friend Class UserData : Inherits UserDataBase +#Region "XML names" + Private Const Name_IsUser As String = "IsUser" + Private Const Name_TrueUserName As String = "TrueUserName" + Private Const Name_TrueBoardName As String = "TrueBoardName" +#End Region +#Region "Structures" + Private Structure BoardInfo + Friend ID As String + Friend Title As String + Friend URL As String + Friend Description As String + Friend UserID As String + Friend UserTitle As String + End Structure +#End Region +#Region "Declarations" + Private ReadOnly Property MySettings As SiteSettings + Get + Return HOST.Source + End Get + End Property + Friend Property TrueUserName As String + Friend Property TrueBoardName As String + Friend Property IsUser As Boolean +#End Region +#Region "Load" + Private Function ReconfUserName() As Boolean + If TrueUserName.IsEmptyString Then + Dim n$() = Name.Split("@") + If n.ListExists Then + TrueUserName = n(0) + IsUser = True + If n.Length > 1 Then TrueBoardName = n(1) : IsUser = False + If Not IsSavedPosts And Not IsSingleObjectDownload Then + Dim l$ = IIf(IsUser, UserLabelName, "Board") + Settings.Labels.Add(l) + Labels.ListAddValue(l, LNC) + Labels.Sort() + End If + Return True + End If + End If + Return False + End Function + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + With Container + If Loading Then + TrueUserName = .Value(Name_TrueUserName) + TrueBoardName = .Value(Name_TrueBoardName) + IsUser = .Value(Name_IsUser).FromXML(Of Boolean)(False) + ReconfUserName() + Else + If ReconfUserName() Then .Value(Name_LabelsName) = Labels.ListToString("|", EDP.ReturnValue) + .Add(Name_TrueUserName, TrueUserName) + .Add(Name_TrueBoardName, TrueBoardName) + .Add(Name_IsUser, IsUser.BoolToInteger) + End If + End With + End Sub +#End Region +#Region "Download overrides" + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + Dim URL$ = String.Empty + Try + If IsSavedPosts Then + IsUser = True + TrueUserName = MySettings.SavedPostsUserName.Value + If TrueUserName.IsEmptyString Then Throw New ArgumentNullException("SavedPostsUserName", "Saved posts user not set") + End If + Dim boards As List(Of BoardInfo) + Dim board As BoardInfo + Dim b$ = TrueBoardName + If Not b.IsEmptyString Then b &= "/" + URL = $"https://www.pinterest.com/{TrueUserName}/{b}" + If IsUser Then + boards = GetBoards(Token) + Else + boards = New List(Of BoardInfo) From {New BoardInfo With {.URL = URL, .ID = ID, .Title = UserSiteName}} + End If + If boards.ListExists Then + For i% = 0 To boards.Count - 1 + ThrowAny(Token) + board = boards(i) + DownloadBoardImages(board, Token) + boards(i) = board + Next + With boards.First + If IsUser Then + If ID.IsEmptyString Then ID = .UserID + UserSiteNameUpdate(.UserTitle) + Else + If ID.IsEmptyString Then ID = .ID + UserSiteNameUpdate(.Title) + UserDescriptionUpdate(.Description) + End If + End With + End If + Catch ex As Exception + ProcessException(ex, Token, $"data downloading error [{URL}]") + End Try + End Sub +#End Region +#Region "Get boards, images" + Private Function GetBoards(ByVal Token As CancellationToken) As List(Of BoardInfo) + Dim URL$ = $"https://www.pinterest.com/{TrueUserName}/" + Try + Dim boards As New List(Of BoardInfo) + Dim b As BoardInfo + Dim r$ + Dim j As EContainer, jj As EContainer + Dim rootNode$() = {"resource_response", "data"} + Dim jErr As New ErrorsDescriber(EDP.SendToLog + EDP.ReturnValue) + Dim urls As List(Of String) = GetDataFromGalleryDL(URL, True) + If urls.ListExists Then urls.RemoveAll(Function(__url) Not __url.Contains("BoardsResource/get/")) + If urls.ListExists Then + For Each URL In urls + ThrowAny(Token) + r = Responser.GetResponse(URL,, EDP.ReturnValue) + If Not r.IsEmptyString Then + j = JsonDocument.Parse(r, jErr) + If Not j Is Nothing Then + If If(j(rootNode)?.Count, 0) > 0 Then + For Each jj In j(rootNode) + b = New BoardInfo With { + .URL = jj.Value("url"), + .Title = TitleHtmlConverter(jj.Value("name")), + .ID = jj.Value("id") + } + If Not b.URL.IsEmptyString Then + b.URL = $"https://www.pinterest.com/{b.URL.StringTrimStart("/").StringTrimEnd("/")}/" + boards.Add(b) + End If + Next + End If + j.Dispose() + End If + End If + Next + End If + Return boards + Catch ex As Exception + ProcessException(ex, Token, $"data (gallery-dl boards) downloading error [{URL}]") + Return Nothing + End Try + End Function + Private Sub DownloadBoardImages(ByRef Board As BoardInfo, ByVal Token As CancellationToken) + Dim bUrl$ = String.Empty + Try + Dim r$ + Dim j As EContainer, jj As EContainer + Dim u As UserMedia + Dim folder$ = If(IsUser, Board.Title.IfNullOrEmpty(Board.ID), String.Empty) + Dim titleExists As Boolean = Not Board.Title.IsEmptyString + Dim i% = -1 + Dim jErr As New ErrorsDescriber(EDP.SendToLog + EDP.ReturnValue) + Dim rootNode$() = {"resource_response", "data"} + Dim images As List(Of Sizes) + Dim imgSelector As Func(Of EContainer, Sizes) = Function(img) New Sizes(img.Value("width"), img.Value("url")) + Dim fullData As Predicate(Of EContainer) = Function(e) e.Count > 5 + Dim l As List(Of String) = GetDataFromGalleryDL(Board.URL, False) + If l.ListExists Then l.RemoveAll(Function(ll) Not ll.Contains("BoardFeedResource/get/")) + If l.ListExists Then + For Each bUrl In l + ThrowAny(Token) + r = Responser.GetResponse(bUrl,, EDP.ReturnValue) + If Not r.IsEmptyString Then + j = JsonDocument.Parse(r, jErr) + If Not j Is Nothing Then + If If(j(rootNode)?.Count, 0) > 0 Then + For Each jj In j(rootNode) + With jj + If .Contains("images") Then + images = .Item("images").Select(imgSelector).ToList + If images.Count > 0 Then + images.Sort() + i += 1 + u = New UserMedia(images(0).Data) With { + .Post = New UserPost(jj.Value("id"), AConvert(Of Date)(jj.Value("created_at"), DateProvider, Nothing)), + .Type = UserMedia.Types.Picture, + .SpecialFolder = folder + } + If i = 0 Then + If Board.Title.IsEmptyString Or Board.ID.IsEmptyString Then + Board.Title = TitleHtmlConverter(.Value({"board"}, "name")) + Board.ID = .Value({"board"}, "id") + End If + Board.UserID = .Value({"board", "owner"}, "id") + Board.UserTitle = TitleHtmlConverter(.Value({"board", "owner"}, "full_name")) + If Not titleExists And IsUser Then + If Not Board.Title.IsEmptyString Then + folder = Board.Title + ElseIf Not Board.ID.IsEmptyString Then + folder = Board.ID + End If + u.SpecialFolder = folder + End If + End If + + If Not u.URL.IsEmptyString Then + If u.Post.Date.HasValue Then + Select Case CheckDatesLimit(u.Post.Date.Value, Nothing) + Case DateResult.Skip : _TempPostsList.ListAddValue(u.Post.ID, LNC) : Continue For + Case DateResult.Exit : Exit Sub + End Select + End If + If Not _TempPostsList.Contains(u.Post.ID) Then + _TempPostsList.ListAddValue(u.Post.ID, LNC) + _TempMediaList.ListAddValue(u, LNC) + Else + Exit For + End If + End If + End If + End If + End With + Next + End If + j.Dispose() + End If + End If + Next + End If + Catch ex As Exception + ProcessException(ex, Token, $"data (gallery-dl images) downloading error [{bUrl}]") + End Try + End Sub +#End Region +#Region "Gallery-DL Support" + Private Class GDLBatch : Inherits GDL.GDLBatch + Private ReadOnly Property Source As UserData + Private ReadOnly IsBoardsRequested As Boolean + Friend Sub New(ByRef s As UserData, ByVal IsBoardsRequested As Boolean) + MyBase.New + Source = s + Me.IsBoardsRequested = IsBoardsRequested + End Sub + Protected Overrides Async Sub OutputDataReceiver(ByVal Sender As Object, ByVal e As DataReceivedEventArgs) + If IsBoardsRequested Then + Await Validate(e.Data) + Else + MyBase.OutputDataReceiver(Sender, e) + Await Validate(e.Data) + End If + End Sub + Protected Overrides Async Function Validate(ByVal Value As String) As Task + If IsBoardsRequested Then + If ErrorOutputData.Count > 0 Then + If Await Task.Run(Of Boolean)(Function() ErrorOutputData.Exists(Function(ee) Not ee.IsEmptyString AndAlso + ee.StartsWith(UrlTextStart))) Then Kill(EDP.None) + End If + Else + If Await Task.Run(Of Boolean)(Function() Not Value.IsEmptyString AndAlso + Source._TempPostsList.Exists(Function(v) Value.Contains(v))) Then Kill(EDP.None) + End If + End Function + End Class + Private Function GetDataFromGalleryDL(ByVal URL As String, ByVal IsBoardsRequested As Boolean) As List(Of String) + Dim command$ = $"gallery-dl --verbose --simulate " + Try + If Not URL.IsEmptyString Then + If MySettings.CookiesNetscapeFile.Exists Then command &= $"--cookies ""{MySettings.CookiesNetscapeFile}"" " + command &= URL + Using batch As New GDLBatch(Me, IsBoardsRequested) + Return GetUrlsFromGalleryDl(batch, command) + End Using + End If + Return Nothing + Catch ex As Exception + HasError = True + LogError(ex, $"GetJson({command})") + Return Nothing + End Try + End Function +#End Region +#Region "DownloadContent" + Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) + DownloadContentDefault(Token) + End Sub +#End Region +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + User = New UserInfo(MySettings.IsMyUser(Data.URL).UserName, HOST) + User.File.Path = Data.File.Path + SeparateVideoFolder = False + ReconfUserName() + DownloadDataF(Token) + Data.Title = UserSiteName + If Data.Title.IsEmptyString Then + Data.Title = TrueUserName + If Not TrueBoardName.IsEmptyString Then Data.Title &= $"/{TrueBoardName}" + End If + Dim additPath$ = TitleHtmlConverter(UserSiteName) + If additPath.IsEmptyString Then additPath = IIf(IsUser, TrueUserName, TrueBoardName) + If Not additPath.IsEmptyString Then + Dim f As SFile = User.File + f.Path = f.PathWithSeparator & additPath + User.File = f + f = Data.File + f.Path = User.File.Path + Data.File = f + End If + End Sub + Protected Overrides Sub DownloadSingleObject_PostProcessing(ByVal Data As IYouTubeMediaContainer, Optional ByVal ResetTitle As Boolean = True) + MyBase.DownloadSingleObject_PostProcessing(Data, Data.Title.IsEmptyString Or Not Data.Title = UserSiteName) + End Sub +#End Region +#Region "Exception" + Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, + Optional ByVal EObj As Object = Nothing) As Integer + Return 0 + End Function + End Class +#End Region +End Namespace \ No newline at end of file diff --git a/SCrawler/API/PornHub/Declarations.vb b/SCrawler/API/PornHub/Declarations.vb index c046642..4ceeec5 100644 --- a/SCrawler/API/PornHub/Declarations.vb +++ b/SCrawler/API/PornHub/Declarations.vb @@ -23,6 +23,7 @@ Namespace API.PornHub Private ReadOnly RegexVideo_Video_Wrong_Option As RParams = RParams.DM("div class=""thumbnail-info-wrapper clearfix.+?[\r\n\s]*?\ Friend ReadOnly Property DownloadGifs As PropertyValue @@ -40,10 +39,7 @@ Namespace API.PornHub #Region "Initializer" Friend Sub New() MyBase.New("PornHub", "pornhub.com") - Responser.CurlPath = $"cURL\curl.exe" - Responser.CurlArgumentsRight = "--ssl-no-revoke" - CurlPathExists = Responser.CurlPath.Exists - Responser.DeclaredError = EDP.ThrowException + With Responser : .CurlSslNoRevoke = True : .CurlInsecure = True : End With DownloadGifsAsMp4 = New PropertyValue(True) DownloadGifs = New PropertyValue(CInt(CheckState.Indeterminate), GetType(Integer)) @@ -55,37 +51,16 @@ Namespace API.PornHub ImageVideoContains = "pornhub" End Sub #End Region -#Region "GetInstance, GetSpecialData" +#Region "GetInstance" 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 Responser = 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 + Return New UserData 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)) + Responser.CurlPath = Settings.CurlFile + Return Settings.UseM3U8 And Settings.CurlFile.Exists And + (Not What = ISiteSettings.Download.SavedPosts OrElse (ACheck(SavedPostsUserName.Value) And Responser.CookiesExists)) End Function #End Region #Region "IsMyUser" @@ -97,23 +72,20 @@ Namespace API.PornHub End If Return Nothing Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, $"[API.PornHub.SiteSettings.IsMyUser({UserURL})]", New ExchangeOptions) + Return ErrorsDescriber.Execute(EDP.SendToLog + 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 +#Region "GetUserUrl" + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) 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 - Return Media.URL_BASE - End Function #End Region #Region "User options" Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) If Options Is Nothing OrElse Not TypeOf Options Is UserExchangeOptions Then Options = New UserExchangeOptions(Me) If OpenForm Then - Using f As New OptionsForm(Options) : f.ShowDialog() : End Using + Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using End If End Sub #End Region diff --git a/SCrawler/API/PornHub/UserData.vb b/SCrawler/API/PornHub/UserData.vb index 8c1e52f..da31b0c 100644 --- a/SCrawler/API/PornHub/UserData.vb +++ b/SCrawler/API/PornHub/UserData.vb @@ -8,6 +8,7 @@ ' but WITHOUT ANY WARRANTY Imports System.Threading Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.Clients @@ -136,6 +137,7 @@ Namespace API.PornHub #Region "Initializer, loader" Friend Sub New() UseInternalM3U8Function = True + UseClientTokens = True End Sub Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) With Container @@ -179,7 +181,11 @@ Namespace API.PornHub Responser.ResetStatus() If PersonType = PersonTypeUser Then Responser.Mode = Responser.Modes.Curl - If IsSavedPosts Then VideoPageModel = VideoPageModels.Favorite + If IsSavedPosts Then + VideoPageModel = VideoPageModels.Favorite + PersonType = PersonTypeUser + NameTrue = MySettings.SavedPostsUserName.Value + End If Dim page% = 1 Dim __continue As Boolean = True @@ -295,7 +301,7 @@ Namespace API.PornHub 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 l As List(Of RegexMatchStruct) = RegexFields(Of RegexMatchStruct)(r, {Regex_Gif_Array}, {1}, EDP.ReturnValue) 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 @@ -336,6 +342,10 @@ Namespace API.PornHub End Sub #End Region #Region "Download photo" + Private Function CreatePhotoFile(ByVal URL As String, ByVal File As SFile) As SFile + Dim pFile$ = RegexReplace(URL, Regex_Photo_File) + If Not pFile.IsEmptyString Then Return New SFile(pFile) Else Return File + End Function 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) @@ -365,7 +375,8 @@ Namespace API.PornHub 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 j As EContainer + Dim jErr As New ErrorsDescriber(EDP.SendToLog + EDP.ReturnValue) Dim albumName$ If PersonType = PersonTypeModel Then URL = String.Format(PhotoUrlPattern_ModelHub, NameTrue) @@ -380,15 +391,16 @@ Namespace API.PornHub 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 + j = 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}\", + .File = CreatePhotoFile(.URL, .File)}), LNC) End If - End Using + j.Dispose() + End If Next l.Clear() End If @@ -444,7 +456,9 @@ Namespace API.PornHub 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) + _TempMediaList.ListAddValue(New UserMedia(url, UTypes.Picture) With { + .SpecialFolder = $"Albums\{AlbumName}\", + .File = CreatePhotoFile(url, .File)}, LNC) End If Catch End Try @@ -468,7 +482,7 @@ Namespace API.PornHub 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 NewUrl$, pFile$ 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 @@ -487,7 +501,8 @@ Namespace API.PornHub NewUrl = RegexReplace(r, Regex_Photo_PornHub_SinglePhoto) If Not NewUrl.IsEmptyString Then m.URL = NewUrl - m.File = NewUrl + pFile = RegexReplace(NewUrl, Regex_Photo_File) + If Not pFile.IsEmptyString Then m.File = pFile Else m.File = NewUrl _TempPostsList.ListAddValue(m.Post.ID, LNC) Else Throw New Exception @@ -511,13 +526,17 @@ Namespace API.PornHub #End Region #End Region #Region "ReparseVideo" - Protected Overrides Sub ReparseVideo(ByVal Token As CancellationToken) + Protected Overloads Overrides Sub ReparseVideo(ByVal Token As CancellationToken) + ReparseVideo(Token, False) + End Sub + Protected Overloads Sub ReparseVideo(ByVal Token As CancellationToken, ByVal CreateFileName As Boolean, + Optional ByRef Data As IYouTubeMediaContainer = Nothing) 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$ + Dim r$, NewUrl$, tmpName$ For i% = _TempMediaList.Count - 1 To 0 Step -1 If _TempMediaList(i).Type = UTypes.VideoPre Then m = _TempMediaList(i) @@ -532,6 +551,14 @@ Namespace API.PornHub Else m.URL = NewUrl m.Type = UTypes.m3u8 + If CreateFileName Then + tmpName = RegexReplace(r, RegexVideoPageTitle) + If Not tmpName.IsEmptyString Then + If Not Data Is Nothing Then Data.Title = tmpName + m.File.Name = TitleHtmlConverter(tmpName) + m.File.Extension = "mp4" + End If + End If _TempMediaList(i) = m End If Else @@ -565,7 +592,7 @@ Namespace API.PornHub 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) + r = Responser.Curl(m.URL_BASE,, eCurl) If Not r.IsEmptyString Then Dim NewUrl$ = CreateVideoURL(r) If Not NewUrl.IsEmptyString Then @@ -591,12 +618,12 @@ Namespace API.PornHub 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) + Protected Overrides Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, ByVal Token As CancellationToken) As SFile + Return M3U8.Download(URL, Responser, DestinationFile, Token, If(UseInternalM3U8Function_UseProgress, Progress, Nothing)) End Function #End Region #Region "CreateVideoURL" - Private Shared Function CreateVideoURL(ByVal r As String) As String + Private Function CreateVideoURL(ByVal r As String) As String Try Dim OutStr$ = String.Empty If Not r.IsEmptyString Then @@ -619,26 +646,18 @@ Namespace API.PornHub End If Return OutStr Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "[API.PornHub.UserData.CreateVideoURL]", String.Empty) + Return ErrorsDescriber.Execute(EDP.SendToLog, 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 Responser, 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 +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + _TempMediaList.Add(New UserMedia(Data.URL, UTypes.VideoPre)) + ReparseVideo(Token, True, Data) + End Sub + Protected Overrides Sub DownloadSingleObject_PostProcessing(ByVal Data As IYouTubeMediaContainer, Optional ByVal ResetTitle As Boolean = True) + MyBase.DownloadSingleObject_PostProcessing(Data, False) + End Sub #End Region #Region "Exceptions" Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, diff --git a/SCrawler/API/PornHub/UserExchangeOptions.vb b/SCrawler/API/PornHub/UserExchangeOptions.vb index 9ccc4b4..8906308 100644 --- a/SCrawler/API/PornHub/UserExchangeOptions.vb +++ b/SCrawler/API/PornHub/UserExchangeOptions.vb @@ -6,18 +6,24 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY +Imports SCrawler.Plugin.Attributes Namespace API.PornHub Friend Class UserExchangeOptions + Friend Property DownloadGifs As Boolean + Friend Property DownloadPhotoOnlyFromModelHub As Boolean + Private ReadOnly Property MySettings As SiteSettings Friend Sub New(ByVal u As UserData) DownloadGifs = u.DownloadGifs DownloadPhotoOnlyFromModelHub = u.DownloadPhotoOnlyFromModelHub + MySettings = u.HOST.Source 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 + MySettings = s End Sub End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Reddit/Channel.vb b/SCrawler/API/Reddit/Channel.vb index fcc9d25..37cf9e7 100644 --- a/SCrawler/API/Reddit/Channel.vb +++ b/SCrawler/API/Reddit/Channel.vb @@ -258,7 +258,8 @@ Namespace API.Reddit .Progress = p, .SaveToCache = True, .SkipExistsUsers = SkipExists, - .ChannelInfo = Me + .ChannelInfo = Me, + .IsChannel = True } With d .SetEnvironment(HOST, CUser, False) @@ -306,7 +307,7 @@ Namespace API.Reddit Friend Function GetEnumerator() As IEnumerator(Of UserPost) Implements IEnumerable(Of UserPost).GetEnumerator Return New MyEnumerator(Of UserPost)(Me) End Function - Friend Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator + Private Function IEnumerable_GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator Return GetEnumerator() End Function #End Region @@ -373,7 +374,7 @@ Namespace API.Reddit Dim l As New List(Of String) If Posts.Count > 0 Or PostsLatest.Count > 0 Then l.ListAddList((From p In PostsAll Where Not p.ID.IsEmptyString Select p.ID), LNC) l.ListAddList(PostsNames, LNC) - If l.Count > 0 Then TextSaver.SaveTextToFile(l.ListToString("|"), FilePosts, True,, EDP.SendInLog) + If l.Count > 0 Then TextSaver.SaveTextToFile(l.ListToString("|"), FilePosts, True,, EDP.SendToLog) End If Using x As New XmlFile With {.AllowSameNames = True, .Name = "Channel"} x.Add(Name_Name, Name) @@ -418,7 +419,7 @@ Namespace API.Reddit CountOfAddedUsers.Clear() CountOfLoadedPostsPerSession.Clear() ChannelExistentUserNames.Clear() - CachePath.Delete(SFO.Path, SFODelete.None, EDP.SendInLog) + CachePath.Delete(SFO.Path, SFODelete.None, EDP.SendToLog) End If disposedValue = True End If diff --git a/SCrawler/API/Reddit/ChannelsCollection.vb b/SCrawler/API/Reddit/ChannelsCollection.vb index 799c496..ddaa9f7 100644 --- a/SCrawler/API/Reddit/ChannelsCollection.vb +++ b/SCrawler/API/Reddit/ChannelsCollection.vb @@ -55,7 +55,7 @@ Namespace API.Reddit Return Nothing End If Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, "[API.Reddit.ChannelsCollection.GetUserFiles]") + Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, "[API.Reddit.ChannelsCollection.GetUserFiles]") End Try End Function Friend Sub UpdateUsersStats() diff --git a/SCrawler/API/Reddit/Declarations.vb b/SCrawler/API/Reddit/Declarations.vb index bd114d7..be26dae 100644 --- a/SCrawler/API/Reddit/Declarations.vb +++ b/SCrawler/API/Reddit/Declarations.vb @@ -15,10 +15,12 @@ Namespace API.Reddit Friend ReadOnly JsonNodesJson() As NodeParams = {New NodeParams("posts", True, True, True, True, 3)} Friend ReadOnly ChannelJsonNodes() As NodeParams = {New NodeParams("data", True, True, True, True, 1), New NodeParams("children", True, True, True)} + Friend ReadOnly SingleJsonNodes() As NodeParams = {New NodeParams("data", True, True, True, True, 2), + New NodeParams("children", True, True, True), + New NodeParams("data", True, True, True, True, 1)} Friend ReadOnly UrlBasePattern As RParams = RParams.DM("(?<=/)([^/]+?\.[\w]{3,4})(?=(\?|\Z))", 0) Friend ReadOnly VideoRegEx As RParams = RParams.DM("http.{0,1}://[^" & Chr(34) & "]+?mp4", 0) Private ReadOnly EUR_PROVIDER As New ANumbers(ANumbers.Cultures.EUR) - Friend ReadOnly DateProvider As New CustomProvider(Function(v, d, p, n, e) ADateTime.ParseUnicodeJS(v, n, e)) - Friend ReadOnly DateProviderChannel As New CustomProvider(Function(v, d, p, n, e) ADateTime.ParseUnicode(AConvert(Of Integer)(v, EUR_PROVIDER, v), n, e)) + Friend ReadOnly UnixDate32ProviderReddit As New CustomProvider(Function(v, d, p, n, e) ADateTime.ParseUnix32(AConvert(Of Integer)(v, EUR_PROVIDER, v), n, e)) End Module End Namespace \ No newline at end of file diff --git a/SCrawler/API/Reddit/M3U8.vb b/SCrawler/API/Reddit/M3U8.vb index 7578d65..4581c8f 100644 --- a/SCrawler/API/Reddit/M3U8.vb +++ b/SCrawler/API/Reddit/M3U8.vb @@ -7,8 +7,11 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Net +Imports System.Threading Imports SCrawler.API.Reddit.M3U8_Declarations +Imports PersonalUtilities.Tools Imports PersonalUtilities.Tools.Web +Imports PersonalUtilities.Forms.Toolbars Imports PersonalUtilities.Functions.RegularExpressions Namespace API.Reddit Namespace M3U8_Declarations @@ -19,7 +22,7 @@ Namespace API.Reddit ''' Audio, Video Friend ReadOnly PlayListRegEx_2 As RParams = RParams.DM("(?<=#EXT-X-BYTERANGE.+?[\r\n]{1,2})(.+)(?=[\r\n]{0,2})", 0, RegexReturn.List) Friend ReadOnly PlayListAudioRegEx As RParams = RParams.DM("(HLS_AUDIO_(\d+)[^""]+)", 0, RegexReturn.List) - Friend ReadOnly DPED As New ErrorsDescriber(EDP.SendInLog + EDP.ReturnValue) + Friend ReadOnly DPED As New ErrorsDescriber(EDP.SendToLog + EDP.ReturnValue) End Module End Namespace Friend NotInheritable Class M3U8 : Implements IDisposable @@ -52,9 +55,12 @@ Namespace API.Reddit Private OutFile As SFile Private VideoFile As SFile Private AudioFile As SFile - Private CachePath As SFile + Private ReadOnly Cache As CacheKeeper + Private ReadOnly CacheFiles As CacheKeeper + Private ReadOnly Property Progress As MyProgress + Private ReadOnly ProgressExists As Boolean #End Region - Private Sub New(ByVal URL As String, ByVal OutFile As SFile) + Private Sub New(ByVal URL As String, ByVal OutFile As SFile, ByVal Progress As MyProgress) PlayListURL = URL BaseURL = RegexReplace(URL, BaseUrlPattern) Video = New List(Of String) @@ -62,7 +68,10 @@ Namespace API.Reddit Me.OutFile = OutFile Me.OutFile.Name = "PlayListFile" Me.OutFile.Extension = "mp4" - CachePath = $"{OutFile.PathWithSeparator}_Cache\{SFile.GetDirectories($"{OutFile.PathWithSeparator}_Cache\",,, EDP.ReturnValue).ListIfNothing.Count + 1}\" + Me.Progress = Progress + ProgressExists = Not Me.Progress Is Nothing + Cache = New CacheKeeper($"{OutFile.PathWithSeparator}_{Base.M3U8Base.TempCacheFolderName}\") + CacheFiles = Cache.NewInstance End Sub #Region "Internal functions" #Region "GetPlaylistUrls" @@ -78,7 +87,7 @@ Namespace API.Reddit If Not r.IsEmptyString Then Dim l As New List(Of Resolution) If Type = Types.Video Then - l = RegexFields(Of Resolution)(r, {PlayListRegEx_1}, {6, 4}) + l = RegexFields(Of Resolution)(r, {PlayListRegEx_1}, {6, 4}, EDP.ReturnValue) Else Try l = RegexFields(Of Resolution)(r, {PlayListAudioRegEx}, {1, 2}) @@ -112,41 +121,44 @@ Namespace API.Reddit End Function #End Region #Region "ConcatData" - Private Overloads Sub ConcatData() - ConcatData(Video, Types.Video, VideoFile) - ConcatData(Audio, Types.Audio, AudioFile) + Private Overloads Sub ConcatData(ByVal Token As CancellationToken) + ConcatData(Video, Types.Video, VideoFile, Token) + ConcatData(Audio, Types.Audio, AudioFile, Token) MergeFiles() End Sub - Private Overloads Sub ConcatData(ByVal Urls As List(Of String), ByVal Type As Types, ByRef TFile As SFile) + Private Overloads Sub ConcatData(ByVal Urls As List(Of String), ByVal Type As Types, ByRef TFile As SFile, ByVal Token As CancellationToken) Try + Token.ThrowIfCancellationRequested() If Urls.ListExists Then - Dim ConcatFile As SFile = OutFile + Dim tmpCache As CacheKeeper = CacheFiles.NewInstance + Dim ConcatFile As SFile = CacheFiles If Type = Types.Audio Then - ConcatFile.Name &= "_AUDIO" + ConcatFile.Name &= "AUDIO" ConcatFile.Extension = "aac" Else - If Audio.Count > 0 Then ConcatFile.Name &= "_VIDEO" + If Audio.Count > 0 Then ConcatFile.Name &= "VIDEO" ConcatFile.Extension = "mp4" End If - 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.ThrowException) + If tmpCache.Validate Then Dim i% - Dim eFiles As New List(Of SFile) - Dim dFile As SFile = CachePath + Dim dFile As SFile = tmpCache.RootDirectory + If ProgressExists Then Progress.Maximum += Urls.Count dFile.Extension = New SFile(Urls(0)).Extension If dFile.Extension.IsEmptyString Then dFile.Extension = "ts" Using w As New WebClient For i = 0 To Urls.Count - 1 + If ProgressExists Then Progress.Perform() + Token.ThrowIfCancellationRequested() dFile.Name = $"ConPart_{i}" w.DownloadFile(Urls(i), dFile) - eFiles.Add(dFile) + tmpCache.AddFile(dFile, True) Next End Using - TFile = FFMPEG.ConcatenateFiles(eFiles, Settings.FfmpegFile, ConcatFile, p, DPED) - eFiles.Clear() + TFile = FFMPEG.ConcatenateFiles(tmpCache, Settings.FfmpegFile.File, ConcatFile, Settings.CMDEncoding,, DPED) End If End If + Catch oex As OperationCanceledException When Token.IsCancellationRequested + Throw oex Catch ex As Exception ErrorsDescriber.Execute(DPED, ex, $"[M3U8.Save({Type})]") End Try @@ -154,25 +166,27 @@ Namespace API.Reddit #End Region Private Sub MergeFiles() Try + Dim p As SFileNumbers = SFileNumbers.Default(OutFile.Name) + Dim f As SFile = SFile.IndexReindex(OutFile,,, p, EDP.ReturnValue) If Not VideoFile.IsEmptyString And Not AudioFile.IsEmptyString Then - Dim p As New SFileNumbers(OutFile.Name,, RParams.DMS("PlayListFile_(\d*)", 1), New ANumbers With {.Format = ANumbers.Formats.General}) - OutFile = FFMPEG.MergeFiles({VideoFile, AudioFile}, Settings.FfmpegFile, OutFile, p, DPED) + OutFile = FFMPEG.MergeFiles({VideoFile, AudioFile}, Settings.FfmpegFile.File, f, Settings.CMDEncoding, p, DPED) Else - OutFile = VideoFile + If f.IsEmptyString Then f = OutFile + If Not SFile.Move(VideoFile, f) Then OutFile = VideoFile End If Catch ex As Exception ErrorsDescriber.Execute(DPED, ex, $"[M3U8.MergeFiles]") End Try End Sub - Friend Function Download() As SFile + Friend Function Download(ByVal Token As CancellationToken) As SFile GetPlaylistUrls() - ConcatData() + ConcatData(Token) Return OutFile End Function #End Region #Region "Statics" - Friend Shared Function Download(ByVal URL As String, ByVal f As SFile) As SFile - Using m As New M3U8(URL, f) : Return m.Download() : End Using + Friend Shared Function Download(ByVal URL As String, ByVal f As SFile, ByVal Token As CancellationToken, ByVal Progress As MyProgress) As SFile + Using m As New M3U8(URL, f, Progress) : Return m.Download(Token) : End Using End Function #End Region #Region "IDisposable Support" @@ -182,7 +196,7 @@ Namespace API.Reddit If disposing Then Video.Clear() Audio.Clear() - CachePath.Delete(SFO.Path, SFODelete.None, DPED) + Cache.Dispose() End If disposedValue = True End If diff --git a/SCrawler/API/Reddit/RedditViewSettingsForm.vb b/SCrawler/API/Reddit/RedditViewSettingsForm.vb index e65fe0e..7ed1653 100644 --- a/SCrawler/API/Reddit/RedditViewSettingsForm.vb +++ b/SCrawler/API/Reddit/RedditViewSettingsForm.vb @@ -18,7 +18,7 @@ Namespace API.Reddit MyOptions = opt MyDefs = New DefaultFormOptions(Me, Settings.Design) End Sub - Private Sub ChannelSettingsForm_Load(sender As Object, e As EventArgs) Handles Me.Load + Private Sub RedditViewSettingsForm_Load(sender As Object, e As EventArgs) Handles Me.Load Try Dim n$ = String.Empty If TypeOf MyOptions Is Channel Then diff --git a/SCrawler/API/Reddit/SiteSettings.vb b/SCrawler/API/Reddit/SiteSettings.vb index df6aaf2..8aa0bba 100644 --- a/SCrawler/API/Reddit/SiteSettings.vb +++ b/SCrawler/API/Reddit/SiteSettings.vb @@ -38,31 +38,28 @@ Namespace API.Reddit End With SavedPostsUserName = New PropertyValue(String.Empty, GetType(String)) UseM3U8 = New PropertyValue(True) - UrlPatternUser = "https://www.reddit.com/user/{0}/" - UrlPatternChannel = "https://www.reddit.com/r/{0}/" + UrlPatternUser = "https://www.reddit.com/{0}/{1}/" ImageVideoContains = "reddit.com" UserRegex = RParams.DM("[htps:/]{7,8}.*?reddit.com/([user]{1,4})/([^/]+)", 0, RegexReturn.ListByMatch, EDP.ReturnValue) End Sub Friend Overrides Function GetInstance(ByVal What As Download) As IPluginContentProvider - Select Case What - Case Download.Main : Return New UserData - Case Download.Channel : Return New UserData With {.SaveToCache = False, .SkipExistsUsers = False, .AutoGetLimits = True} - Case Download.SavedPosts - Dim u As New UserData With {.IsSavedPosts = True} - DirectCast(u, UserDataBase).User = New UserInfo With { - .Name = CStr(AConvert(Of String)(SavedPostsUserName.Value, String.Empty)), - .IsChannel = True - } - Return u - End Select - Return Nothing + Return New UserData End Function + Friend Const ChannelOption As String = "r" Friend Overrides Function IsMyUser(ByVal UserURL As String) As ExchangeOptions Dim l As List(Of String) = RegexReplace(UserURL, UserRegex) - If l.ListExists(3) Then Return New ExchangeOptions(Site, l(2), l(1) = "r") Else Return Nothing + If l.ListExists(3) Then + Dim n$ = l(2) + If Not l(1).IsEmptyString AndAlso l(1) = ChannelOption Then n &= $"@{ChannelOption}" + Return New ExchangeOptions(Site, n) + Else + Return Nothing + End If End Function Friend Overrides Function Available(ByVal What As Download, ByVal Silent As Boolean) As Boolean Try + Dim trueValue As Boolean = Not What = Download.SavedPosts OrElse (Responser.CookiesExists And ACheck(SavedPostsUserName.Value)) + If Not trueValue Then Return False Dim dl As List(Of DownDetector.Data) = DownDetector.GetData("reddit") If dl.ListExists Then dl = dl.Take(4).ToList @@ -76,7 +73,7 @@ Namespace API.Reddit dl.ListToString(vbCr) & vbCr & vbCr & "Do you want to continue parsing Reddit data?", "There are outage reports on Reddit"}, vbYesNo) = vbYes Then UpdateRedGifsToken() - Return True + Return trueValue Else Return False End If @@ -84,28 +81,29 @@ Namespace API.Reddit End If End If UpdateRedGifsToken() - Return True + Return trueValue Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog + EDP.ReturnValue, ex, "[API.Reddit.SiteSettings.Available]", True) + Return ErrorsDescriber.Execute(EDP.SendToLog + 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) - f = $"{f.PathWithSeparator}OptionalPath\" - Return UserData.GetVideoInfo(URL, Responser, f, spf) - End Function Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) If Options Is Nothing OrElse Not TypeOf Options Is RedditViewExchange Then Options = New RedditViewExchange If OpenForm Then Using f As New RedditViewSettingsForm(Options) : f.ShowDialog() : End Using End If End Sub + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String + With DirectCast(User, UserData) : Return String.Format(UrlPatternUser, IIf(.IsChannel, ChannelOption, "user"), .TrueName) : End With + End Function 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}/" + If Not Media.Post.ID.IsEmptyString Then + Return $"https://www.reddit.com/comments/{Media.Post.ID.Split("_").LastOrDefault}/" + Else + Return String.Empty + End If End Function End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Reddit/UserData.vb b/SCrawler/API/Reddit/UserData.vb index 71917d2..87cbd73 100644 --- a/SCrawler/API/Reddit/UserData.vb +++ b/SCrawler/API/Reddit/UserData.vb @@ -10,10 +10,10 @@ Imports System.Net Imports System.Threading Imports SCrawler.API.Base Imports SCrawler.API.Reddit.RedditViewExchange +Imports SCrawler.API.YouTube.Objects Imports SCrawler.Plugin.Hosts Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions -Imports PersonalUtilities.Tools.ImageRenderer Imports PersonalUtilities.Tools.Web.Clients Imports PersonalUtilities.Tools.Web.Documents.JSON Imports UStates = SCrawler.API.Base.UserMedia.States @@ -22,14 +22,20 @@ Imports CView = SCrawler.API.Reddit.IRedditView.View Imports CPeriod = SCrawler.API.Reddit.IRedditView.Period Namespace API.Reddit Friend Class UserData : Inherits UserDataBase : Implements IChannelData, IRedditView +#Region "XML names" + Private Const Name_TrueName As String = "TrueName" +#End Region +#Region "Declarations" + Private Const CannelsLabelName As String = "Channels" + Friend Const CannelsLabelName_ChannelsForm As String = "RChannels" Private ReadOnly Property MySiteSettings As SiteSettings Get Return DirectCast(HOST.Source, SiteSettings) End Get End Property - Private Shared ReadOnly Property DateTrueProvider(ByVal IsChannel As Boolean) As IFormatProvider + Private ReadOnly Property DateTrueProvider(ByVal IsChannel As Boolean) As IFormatProvider Get - Return If(IsChannel, DateProviderChannel, DateProvider) + Return If(IsChannel, UnixDate32ProviderReddit, UnixDate64Provider) End Get End Property Private ReadOnly Property UseM3U8 As Boolean @@ -37,6 +43,9 @@ Namespace API.Reddit Return Settings.UseM3U8 And CBool(DirectCast(HOST.Source, SiteSettings).UseM3U8.Value) End Get End Property + Friend Property IsChannel As Boolean = False + Friend Property TrueName As String = String.Empty +#End Region #Region "Channels Support" #Region "IChannelLimits Support" Friend Property DownloadLimitCount As Integer? Implements IChannelLimits.DownloadLimitCount @@ -60,7 +69,7 @@ Namespace API.Reddit #End Region Friend Property ChannelInfo As Channel Private ReadOnly ChannelPostsNames As List(Of String) - Friend Property SkipExistsUsers As Boolean = True Implements IChannelData.SkipExistsUsers + Friend Property SkipExistsUsers As Boolean = False Implements IChannelData.SkipExistsUsers Private ReadOnly _ExistsUsersNames As List(Of String) Friend Property SaveToCache As Boolean = False Implements IChannelData.SaveToCache Friend Function GetNewChannelPosts() As IEnumerable(Of UserPost) @@ -109,17 +118,49 @@ Namespace API.Reddit ChannelPostsNames = New List(Of String) _ExistsUsersNames = New List(Of String) _CrossPosts = New List(Of String) + UseMD5Comparison = True + StartMD5Checked = True + RemoveExistingDuplicates = False + UseInternalDownloadFileFunction = True + UseInternalM3U8Function = True End Sub #End Region #Region "Load and Update user info" + Private Sub UpdateNames() + If TrueName.IsEmptyString Then + Dim n$() = Name.Split("@") + If n.ListExists Then + If n.Length = 2 Then + TrueName = n(0) + IsChannel = True + ElseIf IsChannel Then + TrueName = Name + Else + TrueName = n(0) + End If + End If + If Not IsSavedPosts Then + Dim l$ = IIf(IsChannel, CannelsLabelName, UserLabelName) + Settings.Labels.Add(l) + Labels.ListAddValue(l, LNC) + Labels.Sort() + End If + End If + End Sub Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) With Container If Loading Then ViewMode = .Value(Name_ViewMode).FromXML(Of Integer)(CInt(CView.New)) ViewPeriod = .Value(Name_ViewPeriod).FromXML(Of Integer)(CInt(CPeriod.All)) + IsChannel = .Value(Name_IsChannel).FromXML(Of Boolean)(False) + TrueName = .Value(Name_TrueName) + UpdateNames() Else + UpdateNames() .Add(Name_ViewMode, CInt(ViewMode)) .Add(Name_ViewPeriod, CInt(ViewPeriod)) + .Add(Name_IsChannel, IsChannel.BoolToInteger) + .Add(Name_TrueName, TrueName) End If End With End Sub @@ -133,7 +174,13 @@ Namespace API.Reddit #Region "Download Overrides" Friend Overrides Sub DownloadData(ByVal Token As CancellationToken) _CrossPosts.Clear() + If CreatedByChannel And Settings.FromChannelDownloadTopUse And Settings.FromChannelDownloadTop > 0 Then _ + DownloadTopCount = Settings.FromChannelDownloadTop.Value + If IsChannel Or IsSavedPosts Then UseMD5Comparison = False + If IsSavedPosts Then TrueName = MySiteSettings.SavedPostsUserName.Value + UpdateNames() If Not IsSavedPosts AndAlso (IsChannel AndAlso Not ChannelInfo Is Nothing) Then + UseMD5Comparison = False EnvirDownloadSet() If Not Responser Is Nothing Then Responser.Dispose() Responser = New Responser @@ -152,7 +199,6 @@ 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 @@ -183,257 +229,195 @@ Namespace API.Reddit Private ReadOnly _CrossPosts As List(Of String) Private Const SiteGfycatKey As String = "gfycat" Private Const SiteRedGifsKey As String = "redgifs" + Private Const Node_CrosspostRootId As String = "crosspostRootId" + Private Const Node_CrosspostParentId As String = "crosspostParentId" + Private Const Node_CrosspostParent As String = "crosspost_parent" Private Sub DownloadDataUser(ByVal POST As String, ByVal Token As CancellationToken) - Const CPRI$ = "crosspostRootId" - Const CPPI$ = "crosspostParentId" + Dim eObj% = 0 + Dim round% = 0 Dim URL$ = String.Empty - Try - Dim PostID$ = String.Empty, PostTmp$ = String.Empty - Dim PostDate$ - Dim n As EContainer, nn As EContainer, s As EContainer - Dim NewPostDetected As Boolean = False - Dim ExistsDetected As Boolean = False - Dim _ItemsBefore% - Dim added As Boolean - Dim __ItemType$ - Dim tmpType As UTypes - Dim IsCrossPost As Predicate(Of EContainer) = Function(e) Not (e.Value(CPRI).IsEmptyString And e.Value(CPPI).IsEmptyString) - Dim CheckNode As Predicate(Of EContainer) = Function(e) Not ParseUserMediaOnly OrElse e("author").XmlIfNothingValue("/").ToLower.Equals(Name.ToLower) - Dim UPicType As Func(Of String, UTypes) = Function(input) IIf(input = "image", UTypes.Picture, UTypes.GIF) - Dim _PostID As Func(Of String) = Function() IIf(PostTmp.IsEmptyString, PostID, PostTmp) + Dim _completed As Boolean = False + Do + round += 1 + Try + Dim PostID$ = String.Empty, PostTmp$ = String.Empty + Dim PostDate$ + Dim n As EContainer, nn As EContainer + Dim NewPostDetected As Boolean = False + Dim ExistsDetected As Boolean = False + Dim IsCrossPost As Predicate(Of EContainer) = Function(e) Not e.Value(Node_CrosspostRootId).IsEmptyString Or Not e.Value(Node_CrosspostParentId).IsEmptyString Or Not e.Value(Node_CrosspostParent).IsEmptyString + Dim CheckNode As Predicate(Of EContainer) = Function(e) Not ParseUserMediaOnly OrElse If(e("author")?.Value, "/").ToLower.Equals(TrueName.StringToLower) + Dim UPicType As Func(Of String, UTypes) = Function(input) IIf(input = "image", UTypes.Picture, UTypes.GIF) + Dim _PostID As Func(Of String) = Function() PostTmp.IfNullOrEmpty(PostID) - URL = $"https://gateway.reddit.com/desktopapi/v1/user/{Name}/posts?rtj=only&allow_quarantined=true&allow_over18=1&include=identity&after={POST}&dist=25&sort={View}&t={Period}&layout=classic" - ThrowAny(Token) - Dim r$ = Responser.GetResponse(URL,, EDP.ThrowException) - If Not r.IsEmptyString Then - Using w As EContainer = JsonDocument.Parse(r).XmlIfNothing - If w.Count > 0 Then - 'TODELETE: moved to 'GetUserInfo' 2023.2.5.0 - 'If UserDescriptionNeedToUpdate() Then UserDescriptionUpdate(w.ItemF({"subredditAboutInfo", 0, "publicDescription"}).XmlIfNothingValue) - n = w.GetNode(JsonNodesJson) - If Not n Is Nothing AndAlso n.Count > 0 Then - For Each nn In n - ThrowAny(Token) - If nn.Count > 0 Then - If CheckNode(nn) Then + URL = $"https://gateway.reddit.com/desktopapi/v1/user/{TrueName}/posts?rtj=only&allow_quarantined=true&allow_over18=1&include=identity&after={POST}&dist=25&sort={View}&t={Period}&layout=classic" + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + Using w As EContainer = JsonDocument.Parse(r).XmlIfNothing + If w.Count > 0 Then + n = w.GetNode(JsonNodesJson) + If Not n Is Nothing AndAlso n.Count > 0 Then + For Each nn In n + ThrowAny(Token) + If nn.Count > 0 Then + If CheckNode(nn) Then - 'Obtain post ID - PostTmp = nn.Name - If PostTmp.IsEmptyString Then PostTmp = nn.Value("id") - If PostTmp.IsEmptyString Then Continue For - 'Check for CrossPost - If IsCrossPost(nn) Then - _CrossPosts.ListAddList({nn.Value(CPRI), nn.Value(CPPI)}, LNC) - Continue For - Else - If Not _CrossPosts.Contains(PostTmp) Then PostID = PostTmp : PostTmp = String.Empty - End If - - 'Download decision - If Not _TempPostsList.Contains(_PostID()) Then - NewPostDetected = True - _TempPostsList.Add(_PostID()) - Else - If Not _CrossPosts.Contains(_PostID()) Then ExistsDetected = True - Continue For - End If - If nn.Contains("created") Then PostDate = nn("created").Value Else PostDate = String.Empty - Select Case CheckDatesLimit(PostDate, DateTrueProvider(IsChannel)) - Case DateResult.Skip : Continue For - Case DateResult.Exit : Exit Sub - End Select - - _ItemsBefore = _TempMediaList.Count - added = True - s = nn.ItemF({"source", "url"}) - If s.XmlIfNothingValue("/").StringContains({$"{SiteRedGifsKey}.com", $"{SiteGfycatKey}.com"}) Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.VideoPre, s.Value, _PostID(), PostDate,, IsChannel), LNC) - ElseIf Not CreateImgurMedia(s.XmlIfNothingValue, _PostID(), PostDate,, IsChannel) Then - s = nn.ItemF({"media"}).XmlIfNothing - __ItemType = s("type").XmlIfNothingValue - Select Case __ItemType - Case "gallery" : If Not DownloadGallery(s, _PostID(), PostDate) Then added = False - Case "image", "gifvideo" - If s.Contains("content") Then - _TempMediaList.ListAddValue(MediaFromData(UPicType(__ItemType), s.Value("content"), - _PostID(), PostDate,, IsChannel), LNC) - Else - added = False - End If - Case "video" - If UseM3U8 AndAlso s("hlsUrl").XmlIfNothingValue("/").ToLower.Contains("m3u8") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.m3u8, s.Value("hlsUrl"), - _PostID(), PostDate,, IsChannel), LNC) - ElseIf Not UseM3U8 AndAlso s("fallback_url").XmlIfNothingValue("/").ToLower.Contains("mp4") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, s.Value("fallback_url"), - _PostID(), PostDate,, IsChannel), LNC) - Else - added = False - End If - Case Else : added = False - End Select - End If - If Not added Then - s = nn.ItemF({"source", "url"}).XmlIfNothing - If Not s.IsEmptyString AndAlso TryFile(s.Value) Then - With s.Value.ToLower - Select Case True - Case .Contains(SiteRedGifsKey), .Contains(SiteGfycatKey) : tmpType = UTypes.VideoPre - Case .Contains("m3u8") : If Settings.UseM3U8 Then tmpType = UTypes.m3u8 - Case .Contains(".gif") And TryFile(s.Value) : tmpType = UTypes.GIF - Case TryFile(s.Value) : tmpType = UTypes.Picture - Case Else : tmpType = UTypes.Undefined - End Select - End With - If Not tmpType = UTypes.Undefined Then - _TempMediaList.ListAddValue(MediaFromData(tmpType, s.Value, _PostID(), PostDate,, IsChannel), LNC) - End If + 'Obtain post ID + PostTmp = nn.Name + If PostTmp.IsEmptyString Then PostTmp = nn.Value("id") + If PostTmp.IsEmptyString Then Continue For + 'Check for CrossPost + If IsCrossPost(nn) Then + _CrossPosts.ListAddList({nn.Value(Node_CrosspostRootId), + nn.Value(Node_CrosspostParentId), + nn.Value(Node_CrosspostParent)}, LNC) + Continue For + Else + If Not _CrossPosts.Contains(PostTmp) Then PostID = PostTmp : PostTmp = String.Empty End If + + 'Download decision + If Not _TempPostsList.Contains(_PostID()) Then + NewPostDetected = True + _TempPostsList.Add(_PostID()) + Else + If Not _CrossPosts.Contains(_PostID()) Then ExistsDetected = True + Continue For + End If + If nn.Contains("created") Then PostDate = nn("created").Value Else PostDate = String.Empty + Select Case CheckDatesLimit(PostDate, DateTrueProvider(IsChannel)) + Case DateResult.Skip : Continue For + Case DateResult.Exit : Exit Sub + End Select + + ParseContainer(nn, _PostID(), PostDate) End If End If - End If - Next + Next + End If End If - End If - End Using - If POST.IsEmptyString And ExistsDetected Then Exit Sub - If Not PostID.IsEmptyString And NewPostDetected Then DownloadDataUser(PostID, Token) - End If - Catch ex As Exception - ProcessException(ex, Token, $"data downloading error [{URL}]") - End Try + End Using + If POST.IsEmptyString And ExistsDetected Then Exit Sub + If Not PostID.IsEmptyString And NewPostDetected Then DownloadDataUser(PostID, Token) + End If + _completed = True + Catch ex As Exception + If ProcessException(ex, Token, $"data downloading error [{URL}]",, eObj) = HttpStatusCode.InternalServerError Then + If round = 2 Then eObj = HttpStatusCode.InternalServerError + Else + _completed = True + End If + End Try + Loop While Not _completed End Sub Private Sub DownloadDataChannel(ByVal POST As String, ByVal Token As CancellationToken) + Dim eObj% = 0 + Dim round% = 0 Dim URL$ = String.Empty - Try - Dim PostID$ = String.Empty - Dim PostDate$, _UserID$, tmpUrl$ - Dim n As EContainer, nn As EContainer, s As EContainer, ss As EContainer - Dim NewPostDetected As Boolean = False - Dim ExistsDetected As Boolean = False - Dim eCount As Predicate(Of EContainer) = Function(e) e.Count > 0 - Dim lDate As Date? + Dim _completed As Boolean = False + Do + round += 1 + Try + Dim PostID$ = String.Empty + Dim PostDate$, _UserID$ + Dim n As EContainer, nn As EContainer, s As EContainer + Dim NewPostDetected As Boolean = False + Dim ExistsDetected As Boolean = False + Dim eCount As Predicate(Of EContainer) = Function(e) e.Count > 0 + Dim lDate As Date? - If IsSavedPosts Then - URL = $"https://www.reddit.com/user/{Name}/saved.json?after={POST}" - Else - URL = $"https://reddit.com/r/{Name}/{View}.json?allow_quarantined=true&allow_over18=1&include=identity&after={POST}&dist=25&sort={View}&t={Period}&layout=classic" - End If + If IsSavedPosts Then + URL = $"https://www.reddit.com/user/{TrueName}/saved.json?after={POST}" + Else + URL = $"https://reddit.com/r/{TrueName}/{View}.json?allow_quarantined=true&allow_over18=1&include=identity&after={POST}&dist=25&sort={View}&t={Period}&layout=classic" + End If - ThrowAny(Token) - Dim r$ = Responser.GetResponse(URL,, EDP.ThrowException) - If Not r.IsEmptyString Then - Using w As EContainer = JsonDocument.Parse(r).XmlIfNothing - If w.Count > 0 Then - n = w.GetNode(ChannelJsonNodes) - If Not n Is Nothing AndAlso n.Count > 0 Then - For Each nn In n - ThrowAny(Token) - s = nn.ItemF({eCount}) - If Not s Is Nothing AndAlso s.Count > 0 Then - PostID = s.Value("name") - If PostID.IsEmptyString AndAlso s.Contains("id") Then PostID = s("id").Value + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then + Using w As EContainer = JsonDocument.Parse(r).XmlIfNothing + If w.Count > 0 Then + n = w.GetNode(ChannelJsonNodes) + If Not n Is Nothing AndAlso n.Count > 0 Then + For Each nn In n + ThrowAny(Token) + s = nn.ItemF({eCount}) + If If(s?.Count, 0) > 0 Then + PostID = s.Value("name") + If PostID.IsEmptyString AndAlso s.Contains("id") Then PostID = s("id").Value - If ChannelPostsNames.Contains(PostID) Then - If ViewMode = CView.New Then ExistsDetected = True Else NewPostDetected = True 'bypass - Continue For - End If - If DownloadLimitCount.HasValue AndAlso _TotalPostsDownloaded >= DownloadLimitCount.Value Then Exit Sub - If Not DownloadLimitPost.IsEmptyString AndAlso DownloadLimitPost = PostID Then Exit Sub - If ViewMode = CView.New AndAlso DownloadLimitDate.HasValue AndAlso _TempMediaList.Count > 0 Then - With (From __u In _TempMediaList Where __u.Post.Date.HasValue Select __u.Post.Date.Value) - If .Count > 0 Then lDate = .Min Else lDate = Nothing - End With - If lDate.HasValue AndAlso lDate.Value <= DownloadLimitDate.Value Then Exit Sub - End If - - If IsSavedPosts Then - If Not _TempPostsList.Contains(PostID) Then - NewPostDetected = True - _TempPostsList.Add(PostID) - Else - ExistsDetected = True + If ChannelPostsNames.Contains(PostID) Then + If ViewMode = CView.New Then ExistsDetected = True Else NewPostDetected = True 'bypass Continue For End If - Else - NewPostDetected = True - End If + If DownloadLimitCount.HasValue AndAlso _TotalPostsDownloaded >= DownloadLimitCount.Value Then Exit Sub + If Not DownloadLimitPost.IsEmptyString AndAlso DownloadLimitPost = PostID Then Exit Sub + If ViewMode = CView.New AndAlso DownloadLimitDate.HasValue AndAlso _TempMediaList.Count > 0 Then + With (From __u In _TempMediaList Where __u.Post.Date.HasValue Select __u.Post.Date.Value) + If .Count > 0 Then lDate = .Min Else lDate = Nothing + End With + If lDate.HasValue AndAlso lDate.Value <= DownloadLimitDate.Value Then Exit Sub + End If - If s.Contains("created") Then PostDate = s("created").Value Else PostDate = String.Empty - _UserID = s.Value("author") - - If Not IsSavedPosts AndAlso SkipExistsUsers AndAlso _ExistsUsersNames.Count > 0 AndAlso - Not _UserID.IsEmptyString AndAlso _ExistsUsersNames.Contains(_UserID) Then - If Not IsSavedPosts AndAlso Not ChannelInfo Is Nothing Then _ - ChannelInfo.ChannelExistentUserNames.ListAddValue(_UserID, LNC) - Continue For - End If - - tmpUrl = s.Value("url") - If Not tmpUrl.IsEmptyString AndAlso tmpUrl.StringContains({"redgifs.com", "gfycat.com"}) Then - If SaveToCache Then - tmpUrl = s.Value({"media", "oembed"}, "thumbnail_url") - If Not tmpUrl.IsEmptyString Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, tmpUrl, PostID, PostDate, _UserID, IsChannel), LNC) - _TotalPostsDownloaded += 1 + If IsSavedPosts Then + If Not _TempPostsList.Contains(PostID) Then + NewPostDetected = True + _TempPostsList.Add(PostID) + Else + ExistsDetected = True + Continue For End If Else - _TempMediaList.ListAddValue(MediaFromData(UTypes.VideoPre, tmpUrl, PostID, PostDate, _UserID, IsChannel), LNC) - _TotalPostsDownloaded += 1 + NewPostDetected = True End If - ElseIf Not s.Value({"media", "reddit_video"}, "fallback_url").IsEmptyString Then - tmpUrl = s.Value({"media", "reddit_video"}, "fallback_url") - If SaveToCache Then - tmpUrl = GetVideoRedditPreview(s) - If Not tmpUrl.IsEmptyString Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, tmpUrl, PostID, PostDate, _UserID, IsChannel, False), LNC) - _TotalPostsDownloaded += 1 - End If - ElseIf UseM3U8 AndAlso Not s.Value({"media", "reddit_video"}, "hls_url").IsEmptyString Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.m3u8, s.Value({"media", "reddit_video"}, "hls_url"), - PostID, PostDate, _UserID, IsChannel), LNC) - Else - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, tmpUrl, PostID, PostDate, _UserID, IsChannel), LNC) - _TotalPostsDownloaded += 1 - End If - ElseIf CreateImgurMedia(tmpUrl, PostID, PostDate, _UserID, IsChannel) Then - _TotalPostsDownloaded += 1 - ElseIf s.Item("media_metadata").XmlIfNothing.Count > 0 Then - DownloadGallery(s, PostID, PostDate, _UserID, SaveToCache) - _TotalPostsDownloaded += 1 - ElseIf s.Contains("preview") Then - ss = s.ItemF({"preview", "images", eCount, "source", "url"}).XmlIfNothing - If Not ss.Value.IsEmptyString Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, ss.Value, PostID, PostDate, _UserID, IsChannel), LNC) - _TotalPostsDownloaded += 1 + + If s.Contains("created") Then PostDate = s("created").Value Else PostDate = String.Empty + _UserID = s.Value("author") + + If Not IsSavedPosts AndAlso SkipExistsUsers AndAlso _ExistsUsersNames.Count > 0 AndAlso + Not _UserID.IsEmptyString AndAlso _ExistsUsersNames.Contains(_UserID) Then + If Not IsSavedPosts AndAlso Not ChannelInfo Is Nothing Then _ + ChannelInfo.ChannelExistentUserNames.ListAddValue(_UserID, LNC) + Continue For End If + + ParseContainer(s, PostID, PostDate, _UserID) End If - End If - Next + Next + End If End If - End If - End Using - If POST.IsEmptyString And ExistsDetected Then Exit Sub - If Not PostID.IsEmptyString And NewPostDetected Then DownloadDataChannel(PostID, Token) - End If - Catch ex As Exception - ProcessException(ex, Token, $"channel data downloading error [{URL}]") - End Try + End Using + If POST.IsEmptyString And ExistsDetected Then Exit Sub + If Not PostID.IsEmptyString And NewPostDetected Then DownloadDataChannel(PostID, Token) + End If + _completed = True + Catch ex As Exception + If ProcessException(ex, Token, $"channel data downloading error [{URL}]",, eObj) = HttpStatusCode.InternalServerError Then + If round = 2 Then eObj = HttpStatusCode.InternalServerError + Else + _completed = True + End If + End Try + Loop While Not _completed End Sub +#End Region +#Region "GetUserInfo" Private Sub GetUserInfo() Try If Not IsSavedPosts And ChannelInfo Is Nothing Then - Dim r$ = Responser.GetResponse($"https://reddit.com/{IIf(IsChannel, "r", "user")}/{Name}/about.json",, EDP.ReturnValue) + Dim r$ = Responser.GetResponse($"https://reddit.com/{IIf(IsChannel, "r", "user")}/{TrueName}/about.json",, EDP.ReturnValue) If Not r.IsEmptyString Then Using j As EContainer = JsonDocument.Parse(r) If Not j Is Nothing AndAlso j.Contains({"data", "subreddit"}) Then + If ID.IsEmptyString Then ID = j.Value({"data"}, "id") With j({"data", "subreddit"}) UserSiteNameUpdate(.Value("title")) UserDescriptionUpdate(.Value("public_description")) Dim dir As SFile = MyFile.CutPath Dim __getFile As Action(Of String) = Sub(ByVal img As String) If Not img.IsEmptyString Then - Dim f As SFile = UrlToFile(img) + Dim f As SFile = CreateFileFromUrl(img) If Not f.Name.IsEmptyString Then If f.Extension.IsEmptyString Then f.Extension = "jpg" f.Path = dir.Path @@ -452,29 +436,149 @@ Namespace API.Reddit End Try End Sub #End Region +#Region "ParseContainer" + Private Function ParseContainer(ByVal e As EContainer, ByVal PostID As String, ByVal PostDate As String, Optional ByVal UserID As String = Nothing, + Optional ByVal AllowReparse As Boolean = True) As Boolean + If Not e Is Nothing Then + Dim UPicType As Func(Of String, UTypes) = Function(input) IIf(input = "image", UTypes.Picture, UTypes.GIF) + Dim eCount As Predicate(Of EContainer) = Function(item) item.Count > 0 + Dim added As Boolean = True + Dim tmpUrl$ = e.Value("url").IfNullOrEmpty(e.Value({"source"}, "url")) + If Not tmpUrl.IsEmptyString AndAlso tmpUrl.StringContains({$"{SiteRedGifsKey}.com", $"{SiteGfycatKey}.com"}) Then + If SaveToCache Then + tmpUrl = e.Value({"media", "oembed"}, "thumbnail_url") + If Not tmpUrl.IsEmptyString Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, tmpUrl, PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + Else + added = False + End If + Else + _TempMediaList.ListAddValue(MediaFromData(UTypes.VideoPre, tmpUrl, PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + End If + ElseIf CreateImgurMedia(tmpUrl, PostID, PostDate, UserID, IsChannel) Then + _TotalPostsDownloaded += 1 + ElseIf DownloadGallery(e, PostID, PostDate, UserID, SaveToCache) Then + _TotalPostsDownloaded += 1 + ElseIf Not If(e({"media"}, "type")?.Value, String.Empty).IsEmptyString Then + With e("media") + Dim t$ = .Item("type").Value + Select Case t + Case "gallery" : If DownloadGallery(.Self, PostID, PostDate) Then _TotalPostsDownloaded += 1 Else added = False + Case "image", "gifvideo" + If .Contains("content") Then + _TempMediaList.ListAddValue(MediaFromData(UPicType(t), .Value("content"), PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + Else + added = False + End If + Case "video" + If UseM3U8 AndAlso .Item("hlsUrl").XmlIfNothingValue("/").ToLower.Contains("m3u8") Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.m3u8, .Value("hlsUrl"), PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + ElseIf Not UseM3U8 AndAlso .Item("fallback_url").XmlIfNothingValue("/").ToLower.Contains("mp4") Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, .Value("fallback_url"), PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + Else + added = False + End If + Case Else : added = False + End Select + End With + ElseIf Not If(e({"media", "reddit_video"}, "fallback_url")?.Value, String.Empty).IsEmptyString Then + tmpUrl = e({"media", "reddit_video"}, "fallback_url").Value + If SaveToCache Then + tmpUrl = GetVideoRedditPreview(e) + If Not tmpUrl.IsEmptyString Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, tmpUrl, PostID, PostDate, UserID, False), LNC) + _TotalPostsDownloaded += 1 + Else + added = False + End If + ElseIf UseM3U8 AndAlso Not If(e({"media", "reddit_video"}, "hls_url")?.Value, String.Empty).IsEmptyString Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.m3u8, e.Value({"media", "reddit_video"}, "hls_url"), PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + Else + _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, tmpUrl, PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + End If + Else + added = False + End If + If Not added Then + If AllowReparse Then + If If(e.ItemF({"crosspost_parent_list", 0})?.Count, 0) > 0 Then + added = ParseContainer(e.ItemF({"crosspost_parent_list", 0}), PostID, PostDate, UserID, True) + Else + Dim tPostId$ = e.Value(Node_CrosspostParent).IfNullOrEmpty(e.Value(Node_CrosspostParentId)).IfNullOrEmpty(e.Value(Node_CrosspostRootId)) + Dim r$ = Responser.GetResponse($"https://www.reddit.com/comments/{tPostId.Split("_").LastOrDefault}/.json",, EDP.ReturnValue) + If Not r.IsEmptyString Then + Using j As EContainer = JsonDocument.Parse(r, EDP.ReturnValue) + If j.ListExists Then + With j.ItemF({0, "data", "children", 0, "data"}) + If .ListExists Then added = ParseContainer(.Self, PostID, PostDate, UserID, False) + End With + End If + End Using + End If + End If + End If + If Not added Then + Dim node As EContainer = e({"source", "url"}) + Dim tmpType As UTypes = UTypes.Undefined + If Not If(node?.Value, String.Empty).IsEmptyString Then + With node.Value.ToLower + Select Case True + Case .Contains(SiteRedGifsKey), .Contains(SiteGfycatKey) : If Not SaveToCache Then tmpType = UTypes.VideoPre + Case .Contains("m3u8") : If Settings.UseM3U8 And Not SaveToCache Then tmpType = UTypes.m3u8 + Case .Contains(".gif") And TryFile(node.Value) : tmpType = UTypes.GIF + Case TryFile(node.Value) : tmpType = UTypes.Picture + Case Else : tmpType = UTypes.Undefined + End Select + End With + If Not tmpType = UTypes.Undefined Then + _TempMediaList.ListAddValue(MediaFromData(tmpType, node.Value, PostID, PostDate, UserID), LNC) + added = True + End If + End If + If Not added And e.Contains("preview") Then + tmpUrl = If(e.ItemF({"preview", "images", eCount, "source", "url"})?.Value, String.Empty) + If Not tmpUrl.IsEmptyString Then + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, tmpUrl, PostID, PostDate, UserID), LNC) + _TotalPostsDownloaded += 1 + added = True + End If + End If + End If + End If + Return added + Else + Return False + End If + End Function +#End Region #Region "Download Base Functions" Private Function CreateImgurMedia(ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, Optional ByVal _UserID As String = "", Optional ByVal IsChannel As Boolean = False) As Boolean If Not _URL.IsEmptyString AndAlso _URL.Contains("imgur") Then If _URL.StringContains({".jpg", ".png", ".jpeg"}) Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL, PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL, PostID, PostDate, _UserID), LNC) ElseIf _URL.Contains(".gifv") Then If SaveToCache Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL.Replace(".gifv", ".gif"), - PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL.Replace(".gifv", ".gif"), PostID, PostDate, _UserID), LNC) Else - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, _URL.Replace(".gifv", ".mp4"), - PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, _URL.Replace(".gifv", ".mp4"), PostID, PostDate, _UserID), LNC) End If ElseIf _URL.Contains(".mp4") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, _URL, PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, _URL, PostID, PostDate, _UserID), LNC) ElseIf _URL.Contains(".gif") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.GIF, _URL, PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.GIF, _URL, PostID, PostDate, _UserID), LNC) Else Dim obj As IEnumerable(Of UserMedia) = Imgur.Envir.GetVideoInfo(_URL, EDP.ReturnValue) If Not obj.ListExists Then If Not TryFile(_URL) Then _URL &= ".jpg" - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL, PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, _URL, PostID, PostDate, _UserID), LNC) Else Dim ut As UTypes Dim m As UserMedia @@ -489,7 +593,7 @@ Namespace API.Reddit Case "gif" : ut = UTypes.GIF Case Else : ut = UTypes.Picture : .File.Extension = "jpg" End Select - m = MediaFromData(ut, _URL, PostID, PostDate, _UserID, IsChannel) + m = MediaFromData(ut, _URL, PostID, PostDate, _UserID) m.URL = .URL m.File = .File.File _TempMediaList.ListAddValue(m, LNC) @@ -504,17 +608,22 @@ Namespace API.Reddit Return False End If End Function - Private Function DownloadGallery(ByVal w As EContainer, ByVal PostID As String, ByVal PostDate As String, + Private Function DownloadGallery(ByVal e As EContainer, ByVal PostID As String, ByVal PostDate As String, Optional ByVal _UserID As String = Nothing, Optional ByVal FirstOnly As Boolean = False) As Boolean Try Dim added As Boolean = False - Dim cn$ = IIf(IsChannel, "media_metadata", "mediaMetadata") - If Not w Is Nothing AndAlso w(cn).XmlIfNothing.Count > 0 Then + Dim node As EContainer = Nothing + If e.Contains("media_metadata") Then + node = e("media_metadata") + ElseIf e.Contains("mediaMetadata") Then + node = e("mediaMetadata") + End If + If If(node?.Count, 0) > 0 Then Dim t As EContainer - For Each n As EContainer In w(cn) + For Each n As EContainer In node t = n.ItemF({"s", "u"}) If Not t Is Nothing AndAlso Not t.Value.IsEmptyString Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, t.Value, PostID, PostDate, _UserID, IsChannel), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, t.Value, PostID, PostDate, _UserID), LNC) added = True If FirstOnly Then Exit For End If @@ -558,6 +667,8 @@ Namespace API.Reddit Return String.Empty End Try End Function +#End Region +#Region "ReparseVideo" Protected Overrides Sub ReparseVideo(ByVal Token As CancellationToken) Dim RedGifsResponser As Responser = Nothing Try @@ -619,6 +730,8 @@ Namespace API.Reddit If Not RedGifsResponser Is Nothing Then RedGifsResponser.Dispose() End Try End Sub +#End Region +#Region "ReparseMissing" Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken) Dim rList As New List(Of Integer) Dim RedGifsResponser As Responser = Nothing @@ -628,21 +741,33 @@ Namespace API.Reddit Dim RedGifsHost As SettingsHost = Settings(RedGifs.RedGifsSiteKey) RedGifsResponser = RedGifsHost.Responser.Copy Dim m As UserMedia, m2 As UserMedia + Dim r$ + Dim j As EContainer + Dim lastCount%, li% For i% = 0 To _ContentList.Count - 1 m = _ContentList(i) If m.State = UStates.Missing AndAlso Not m.Post.ID.IsEmptyString Then ThrowAny(Token) - If Not m.URL.IsEmptyString AndAlso m.URL.Contains(SiteRedGifsKey) Then - m2 = RedGifs.UserData.GetDataFromUrlId(m.URL, False, RedGifsResponser, RedGifsHost) - If m2.State = RedGifs.UserData.DataGone Then - rList.Add(i) - ElseIf Not m2.Type = UTypes.Undefined And Not m2.State = UStates.Missing Then - m.Type = m2.Type - m.File = m2.File - m.URL_BASE = m.URL - m.URL = m2.URL - rList.Add(i) - _TempMediaList.ListAddValue(m, LNC) + r = Responser.GetResponse($"https://www.reddit.com/comments/{m.Post.ID.Split("_").LastOrDefault}/.json",, EDP.ReturnValue) + If Not r.IsEmptyString Then + j = JsonDocument.Parse(r, EDP.ReturnValue) + If Not j Is Nothing Then + If j.Count > 0 Then + lastCount = _TempMediaList.Count + With j.GetNode(SingleJsonNodes) + If .ListExists AndAlso ParseContainer(.Self, m.Post.ID, String.Empty) Then + If lastCount <> _TempMediaList.Count Then + For li = IIf(lastCount < 0, 0, lastCount) To _TempMediaList.Count - 1 + m2 = _TempMediaList(i) + m2.Post.Date = m.Post.Date + _TempMediaList(i) = m2 + Next + End If + rList.Add(i) + End If + End With + End If + j.Dispose() End If End If End If @@ -658,82 +783,27 @@ Namespace API.Reddit End If End Try End Sub - Private Sub ParsePost(ByVal URL As String) - Try - If Not URL.IsEmptyString Then - Dim __id$ = RegexReplace(URL, RParams.DMS("comments/([^/]+)", 1, EDP.ReturnValue)) - If Not __id.IsEmptyString Then - URL = $"https://www.reddit.com/comments/{__id.Split("_").LastOrDefault}/.json" - Dim r$ = Responser.GetResponse(URL,, EDP.ReturnValue) - If Not r.IsEmptyString Then - Using j As EContainer = JsonDocument.Parse(r).XmlIfNothing - With j.ItemF({0, "data", "children", 0, "data"}) - If .ListExists Then - If .Contains({"media"}, "reddit_video") Then - With .Item({"media"}, "reddit_video") - If UseM3U8 AndAlso .Item("hls_url").XmlIfNothingValue("/").ToLower.Contains("m3u8") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.m3u8, .Value("hls_url"), __id, String.Empty), LNC) - ElseIf Not UseM3U8 AndAlso .Item("fallback_url").XmlIfNothingValue("/").ToLower.Contains("mp4") Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, .Value("fallback_url"), __id, String.Empty), LNC) - End If - End With - ElseIf Not .Value("url").IsEmptyString Then - If .Value("url").StringContains({$"{SiteRedGifsKey}.com", $"{SiteGfycatKey}.com"}) Then - _TempMediaList.ListAddValue(MediaFromData(UTypes.VideoPre, .Value("url"), __id, String.Empty), LNC) - Else - CreateImgurMedia(.Value("url"), __id, String.Empty) - End If - End If - End If - End With - End Using - End If - End If - End If - Catch ex As Exception - ErrorsDescriber.Execute(EDP.SendInLog, ex, $"API.Reddit.ParsePost({URL})") - End Try +#End Region +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim __id$ = RegexReplace(Data.URL, RParams.DMS("comments/([^/]+)", 1, EDP.ReturnValue)) + If Not __id.IsEmptyString Then + User.File = Data.File + User.File.Name = String.Empty + User.File.Extension = String.Empty + _ContentList.Add(New UserMedia With {.State = UStates.Missing, .Post = __id}) + ReparseMissing(Token) + ReparseVideo(Token) + End If End Sub - Private Class AbsProgress : Inherits PersonalUtilities.Forms.Toolbars.MyProgress - Public Overrides Sub Perform(Optional ByVal Value As Double = 1) - End Sub - End Class - Friend Shared Function GetVideoInfo(ByVal URL As String, ByVal resp As Responser, ByVal f As SFile, ByVal SpecialFolder As String) As IEnumerable(Of UserMedia) - Try - If Not URL.IsEmptyString Then - Using r As New UserData - r.SetEnvironment(Settings(RedditSiteKey), Nothing, False, False) - r.Responser = New Responser - r.Responser.Copy(resp) - r.ParsePost(URL) - If r._TempMediaList.Count > 0 Then - r.ReparseVideo(Nothing) - If r._TempMediaList.Count > 0 Then - r._ContentNew.AddRange(r._TempMediaList) - r.Progress = New AbsProgress - r.User.File.Path = f.Path - r.SeparateVideoFolder = False - r.DownloadContent(Nothing) - If r._ContentNew.Exists(Function(c) c.State = UStates.Downloaded) Then _ - Return {New UserMedia With {.State = UStates.Downloaded, .SpecialFolder = SpecialFolder}} - End If - End If - End Using - End If - Return Nothing - Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, $"[API.Reddit.UserData.GetVideoInfo({URL})]") - End Try - End Function #End Region #Region "Structure creator" - Protected Shared Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, - Optional ByVal _UserID As String = "", Optional ByVal IsChannel As Boolean = False, - Optional ByVal ReplacePreview As Boolean = True) As UserMedia + Private Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, + Optional ByVal _UserID As String = "", Optional ByVal ReplacePreview As Boolean = True) As UserMedia If _URL.IsEmptyString And t = UTypes.Picture Then Return Nothing _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) Dim m As New UserMedia(_URL, t) With {.Post = New UserPost With {.ID = PostID, .UserID = _UserID}} - If t = UTypes.Picture Or t = UTypes.GIF Then m.File = UrlToFile(m.URL) Else m.File = Nothing + If t = UTypes.Picture Or t = UTypes.GIF Then m.File = CreateFileFromUrl(m.URL) Else m.File = Nothing If ReplacePreview And m.URL.Contains("preview") Then m.URL = $"https://i.redd.it/{m.File.File}" If Not PostDate.IsEmptyString Then m.Post.Date = AConvert(Of Date)(PostDate, DateTrueProvider(IsChannel), Nothing) Else m.Post.Date = Nothing Return m @@ -741,203 +811,67 @@ Namespace API.Reddit Private Function TryFile(ByVal URL As String) As Boolean Try If Not URL.IsEmptyString AndAlso URL.StringContains({".jpg", ".png", ".jpeg"}) Then - Dim f As SFile = CStr(RegexReplace(URL, FilesPattern)) - Return Not f.File.IsEmptyString + Return Not CreateFileFromUrl(URL).IsEmptyString + Else + Return False End If - Return False Catch ex As Exception Return False End Try End Function - Private Shared Function UrlToFile(ByVal URL As String) As SFile - Return CStr(RegexReplace(URL, FilesPattern)) + Protected Overrides Function CreateFileFromUrl(ByVal URL As String) As SFile + Return New SFile(CStr(RegexReplace(URL, FilesPattern))) End Function #End Region +#Region "DownloadContent" + Private _RedGifsResponser As Responser = Nothing Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) - Dim RedGifsResponser As Responser = Nothing - Try - Const _RFN$ = "RedditVideo" - Const RFN$ = _RFN & "{0}" - Dim i% - Dim dCount% = 0, dTotal% = 0 - ThrowAny(Token) - If _ContentNew.Count > 0 Then - _ContentNew.RemoveAll(Function(c) c.URL.IsEmptyString) - If _ContentNew.Count > 0 Then - RedGifsResponser = Settings(RedGifs.RedGifsSiteKey).Responser.Copy - MyFile.Exists(SFO.Path) - Dim MissingErrorsAdd As Boolean = Settings.AddMissingErrorsToLog - Dim IsImgurStuff As Boolean - Dim MyDir$ - If Not IsSavedPosts AndAlso (IsChannel And SaveToCache And Not ChannelInfo Is Nothing) Then - MyDir = ChannelInfo.CachePath.PathNoSeparator - Else - MyDir = MyFile.CutPath.PathNoSeparator - End If - Dim StartRFN% = 0 - If _ContentNew.Exists(Function(c) c.Type = UTypes.Video And c.URL.Contains("redd.it")) Then - StartRFN = SFile.Indexed_GetMaxIndex($"{MyDir}\{IIf(SeparateVideoFolderF, "Video\", String.Empty)}{_RFN}.mp4",, New SFileNumbers(_RFN, String.Empty), EDP.ReturnValue) - End If - Dim HashList As New List(Of String) - If _ContentList.Count > 0 Then HashList.ListAddList((From h In _ContentList Where Not h.MD5.IsEmptyString Select h.MD5), LNC) - Dim f As SFile - Dim v As UserMedia - Dim cached As Boolean = IsChannel And SaveToCache - Dim vsf As Boolean = SeparateVideoFolderF - Dim UseMD5 As Boolean = Not IsChannel Or (Not cached And Settings.ChannelsRegularCheckMD5) - Dim bDP As New ErrorsDescriber(EDP.None) - Dim RGRERROR As New ErrorsDescriber(EDP.ThrowException) - Dim ImgurUrls As New List(Of String) - Dim TryBytes As Func(Of String, Imaging.ImageFormat, String) = - Function(ByVal __URL As String, ByVal ImgFormat As Imaging.ImageFormat) As String - Try - Return ByteArrayToString(GetMD5(SFile.GetBytesFromNet(__URL, bDP), ImgFormat)) - Catch hash_ex As Exception - Return String.Empty - End Try - End Function - Dim MD5BS As Func(Of String, UTypes, - SFile, Boolean, String) = Function(ByVal __URL As String, ByVal __MT As UTypes, - ByVal __File As SFile, ByVal __IsBase As Boolean) As String - Try - ImgurUrls.Clear() - Dim ImgFormat As Imaging.ImageFormat - If __MT = UTypes.GIF Then - ImgFormat = Imaging.ImageFormat.Gif - ElseIf __IsBase Then - ImgFormat = GetImageFormat(CStr(RegexReplace(__URL, UrlBasePattern))) - Else - ImgFormat = GetImageFormat(__File) - End If - - Dim tmpBytes$ = TryBytes(__URL, ImgFormat) - If tmpBytes.IsEmptyString And Not __MT = UTypes.GIF Then - ImgFormat = Imaging.ImageFormat.Png - tmpBytes = TryBytes(__URL, ImgFormat) - If Not tmpBytes.IsEmptyString Then Return tmpBytes - Else - Return tmpBytes - End If - - If tmpBytes.IsEmptyString And Not __MT = UTypes.GIF And __URL.Contains("imgur.com") Then - For c% = 0 To 1 - If c = 0 Then - ImgurUrls.ListAddList(Imgur.Envir.GetGallery(__URL)) - Else - ImgurUrls.ListAddValue(Imgur.Envir.GetImage(__URL)) - End If - If ImgurUrls.Count > 0 Then Exit For - Next - End If - Return tmpBytes - Catch hash_ex As Exception - Return String.Empty - End Try - End Function - Dim m$ - Using w As New WebClient - If vsf Then CSFileP($"{MyDir}\Video\").Exists(SFO.Path) - Progress.Maximum += _ContentNew.Count - For i = 0 To _ContentNew.Count - 1 - ThrowAny(Token) - v = _ContentNew(i) - v.State = UStates.Tried - If v.File.IsEmptyString Then - f = UrlToFile(v.URL) - Else - f = v.File - End If - f.Separator = "\" - m = String.Empty - If (v.Type = UTypes.Picture Or v.Type = UTypes.GIF) And UseMD5 Then - m = MD5BS(v.URL, v.Type, f, False) - If ImgurUrls.Count = 0 AndAlso m.IsEmptyString AndAlso Not v.URL_BASE.IsEmptyString AndAlso Not v.URL_BASE = v.URL Then - m = MD5BS(v.URL_BASE, v.Type, f, True) - If Not m.IsEmptyString Then v.URL = v.URL_BASE - End If - End If - - If (Not m.IsEmptyString AndAlso Not HashList.Contains(m)) Or Not (v.Type = UTypes.Picture Or - v.Type = UTypes.GIF) Or Not UseMD5 Or ImgurUrls.Count > 0 Then - IsImgurStuff = ImgurUrls.Count > 0 - Do - If Not cached And Not m.IsEmptyString Then HashList.Add(m) - v.MD5 = m - If ImgurUrls.Count > 0 Then - If ImgurUrls(0).IsEmptyString Then ImgurUrls.RemoveAt(0) : Continue Do - f = UrlToFile(ImgurUrls(0)) - If f.Extension.IsEmptyString Then f.Extension = "gif" - If f.Name.IsEmptyString Then - f.Path = MyDir - f.Name = $"ImgurImg_{v.File.Name}" - f = SFile.Indexed_IndexFile(f,,, EDP.ReturnValue) - End If - End If - If f.Extension = "webp" And Settings.DownloadNativeImageFormat Then f.Extension = "jpg" - f.Path = MyDir - Try - If (v.Type = UTypes.Video Or v.Type = UTypes.m3u8 Or (ImgurUrls.Count > 0 AndAlso f.Extension = "mp4")) And - vsf Then f.Path = $"{f.PathWithSeparator}Video" - If v.Type = UTypes.Video AndAlso v.URL.Contains("redd.it") Then - StartRFN += 1 - f.Name = String.Format(RFN, StartRFN) - End If - If v.Type = UTypes.m3u8 Then - f = M3U8.Download(v.URL, f) - ElseIf ImgurUrls.Count > 0 Then - w.DownloadFile(ImgurUrls(0), f.ToString) - ElseIf v.URL.Contains(SiteRedGifsKey) Then - RedGifsResponser.DownloadFile(v.URL, f, RGRERROR) - Else - w.DownloadFile(v.URL, f.ToString) - End If - If Not v.Type = UTypes.m3u8 Or Not f.IsEmptyString Then - Select Case v.Type - Case UTypes.Picture, UTypes.GIF : DownloadedPictures(False) += 1 - Case UTypes.Video, UTypes.m3u8 : DownloadedVideos(False) += 1 - End Select - If Not IsChannel Or Not SaveToCache Then - v.File = ChangeFileNameByProvider(f, v) - Else - v.File = f - End If - v.Post.CachedFile = f - v.State = UStates.Downloaded - dCount += 1 - End If - Catch wex As Exception - If Not IsChannel Then - If Not IsImgurStuff And MissingErrorsAdd Then ErrorDownloading(f, v.URL) - v.Attempts += 1 - v.State = UStates.Missing - End If - End Try - If ImgurUrls.Count > 0 Then ImgurUrls.RemoveAt(0) - Loop While ImgurUrls.Count > 0 - Else - v.State = UStates.Skipped - End If - _ContentNew(i) = v - If (CreatedByChannel And Settings.FromChannelDownloadTopUse And dCount >= Settings.FromChannelDownloadTop) Or - (DownloadTopCount.HasValue AndAlso dCount >= DownloadTopCount.Value) Then - Progress.Perform(_ContentNew.Count - dTotal) - Exit Sub - Else - dTotal += 1 - Progress.Perform() - End If - Next - End Using - End If - End If - Catch iex As IndexOutOfRangeException When Disposed - Catch oex As OperationCanceledException When Token.IsCancellationRequested - Catch dex As ObjectDisposedException When Disposed - Catch ex As Exception - LogError(ex, "content downloading error") - HasError = True - End Try + If _ContentNew.Count > 0 Then + Try + If Not _RedGifsResponser Is Nothing Then _RedGifsResponser.Dispose() + _RedGifsResponser = Settings(RedGifs.RedGifsSiteKey).Responser.Copy + DownloadContentDefault(Token) + Finally + If Not _RedGifsResponser Is Nothing Then _RedGifsResponser.Dispose() : _RedGifsResponser = Nothing + End Try + End If End Sub + Protected Overrides Function DownloadContentDefault_GetRootDir() As String + If Not IsSavedPosts AndAlso (IsChannel And SaveToCache And Not ChannelInfo Is Nothing) Then + Return ChannelInfo.CachePath.PathNoSeparator + Else + Return MyBase.DownloadContentDefault_GetRootDir() + End If + End Function + Protected Overrides Sub DownloadContentDefault_PostProcessing(ByRef m As UserMedia, ByVal File As SFile, ByVal Token As CancellationToken) + m.Post.CachedFile = File + MyBase.DownloadContentDefault_PostProcessing(m, File, Token) + End Sub + Protected Overrides Function DownloadContentDefault_ProcessDownloadException() As Boolean + Return Not IsChannel Or Not SaveToCache + End Function + Protected Overrides Function DownloadFile(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, ByVal Token As CancellationToken) As SFile + If _RedGifsResponser.DownloadFile(URL, DestinationFile, EDP.ThrowException) Then + Return DestinationFile + Else + Return Nothing + End If + End Function + Protected Overrides Function ValidateDownloadFile(ByVal URL As String, ByVal Media As UserMedia, ByRef Interrupt As Boolean) As Boolean + Return URL.Contains(SiteRedGifsKey) + End Function + Protected Overrides Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, ByVal Token As CancellationToken) As SFile + Return M3U8.Download(URL, DestinationFile, Token, IIf(IsSingleObjectDownload, Progress, Nothing)) + End Function + Protected Overrides Function ChangeFileNameByProvider(ByVal f As SFile, ByVal m As UserMedia) As SFile + If Not IsChannel Or Not SaveToCache Then + Return MyBase.ChangeFileNameByProvider(f, m) + Else + Return f + End If + End Function +#End Region +#Region "Exception" 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 With Responser @@ -949,6 +883,9 @@ Namespace API.Reddit MyMainLOG = $"[{CInt(Responser.StatusCode)}] Reddit is currently unavailable ({ToString()})" ElseIf .StatusCode = HttpStatusCode.GatewayTimeout Then Return 1 + ElseIf .StatusCode = HttpStatusCode.InternalServerError Then + If Not IsNothing(EObj) AndAlso IsNumeric(EObj) AndAlso CInt(EObj) = HttpStatusCode.InternalServerError Then Return 1 + Return HttpStatusCode.InternalServerError Else If Not FromPE Then LogError(ex, Message) : HasError = True Return 0 @@ -956,9 +893,12 @@ Namespace API.Reddit End With Return 1 End Function +#End Region +#Region "IDisposable Support" Protected Overrides Sub Dispose(ByVal disposing As Boolean) If Not disposedValue And disposing Then ChannelPostsNames.Clear() : _ExistsUsersNames.Clear() : _CrossPosts.Clear() MyBase.Dispose(disposing) End Sub +#End Region End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Redgifs/Declarations.vb b/SCrawler/API/Redgifs/Declarations.vb index 3fd01e6..401c42c 100644 --- a/SCrawler/API/Redgifs/Declarations.vb +++ b/SCrawler/API/Redgifs/Declarations.vb @@ -11,7 +11,6 @@ Namespace API.RedGifs Friend Module Declarations Friend Const RedGifsSiteKey As String = "AndyProgram_RedGifs" Friend Const RedGifsSite As String = "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, CType(Function(Input$) Input.StringToLower.StringTrim, Func(Of String, String))) diff --git a/SCrawler/API/Redgifs/SiteSettings.vb b/SCrawler/API/Redgifs/SiteSettings.vb index dfdb5b5..e664b28 100644 --- a/SCrawler/API/Redgifs/SiteSettings.vb +++ b/SCrawler/API/Redgifs/SiteSettings.vb @@ -14,8 +14,6 @@ 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 -Imports UStates = SCrawler.API.Base.UserMedia.States Namespace API.RedGifs Friend Class SiteSettings : Inherits SiteSettingsBase @@ -32,18 +30,17 @@ Namespace API.RedGifs End Property Friend ReadOnly Property Token As PropertyValue + + Private ReadOnly Property UserAgent As PropertyValue Friend ReadOnly Property TokenLastDateUpdated As PropertyValue Private Const TokenName As String = "authorization" #Region "TokenUpdateInterval" Friend ReadOnly Property TokenUpdateInterval As PropertyValue - Private Class TokenIntervalProvider : Implements IFieldsCheckerProvider - Private Property ErrorMessage As String Implements IFieldsCheckerProvider.ErrorMessage - Private Property Name As String Implements IFieldsCheckerProvider.Name - Private Property TypeError As Boolean Implements IFieldsCheckerProvider.TypeError - Private Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, - Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object Implements ICustomProvider.Convert + Private Class TokenIntervalProvider : Inherits FieldsCheckerProviderBase + Public Overrides Function Convert(ByVal Value As Object, ByVal DestinationType As Type, ByVal Provider As IFormatProvider, + Optional ByVal NothingArg As Object = Nothing, Optional ByVal e As ErrorsDescriber = Nothing) As Object TypeError = False ErrorMessage = String.Empty If Not ACheck(Of Integer)(Value) Then @@ -52,12 +49,10 @@ Namespace API.RedGifs Return Value Else ErrorMessage = $"The value of [{Name}] field must be greater than or equal to 1" + HasError = True End If Return Nothing End Function - Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat - Throw New NotImplementedException("[GetFormat] is not available in the context of [TokenIntervalProvider]") - End Function End Class Private ReadOnly Property TokenUpdateIntervalProvider As IFormatProvider @@ -68,12 +63,14 @@ Namespace API.RedGifs MyBase.New(RedGifsSite, "redgifs.com") Dim t$ = String.Empty With Responser - Dim b As Boolean = Not .Mode = Responser.Modes.WebClient .Mode = Responser.Modes.WebClient + If Not .UserAgentExists Then .UserAgent = ParserUserAgent + .ClientWebUseCookies = False + .ClientWebUseHeaders = True t = .Headers.Value(TokenName) - If b Then .SaveSettings() End With - Token = New PropertyValue(t, GetType(String), Sub(v) UpdateResponse(v)) + Token = New PropertyValue(t, GetType(String), Sub(v) UpdateResponse(NameOf(Token), v)) + UserAgent = New PropertyValue(Responser.UserAgent, GetType(String), Sub(v) UpdateResponse(NameOf(UserAgent), v)) TokenLastDateUpdated = New PropertyValue(Now.AddYears(-1), GetType(Date)) TokenUpdateInterval = New PropertyValue(60 * 12, GetType(Integer)) TokenUpdateIntervalProvider = New TokenIntervalProvider @@ -83,8 +80,11 @@ Namespace API.RedGifs End Sub #End Region #Region "Response updater" - Private Sub UpdateResponse(ByVal Value As String) - Responser.Headers.Add(TokenName, Value) + Private Sub UpdateResponse(ByVal Name As String, ByVal Value As String) + Select Case Name + Case NameOf(Token) : Responser.Headers.Add(TokenName, Value) + Case NameOf(UserAgent) : Responser.UserAgent = Value + End Select Responser.SaveSettings() End Sub #End Region @@ -101,16 +101,18 @@ Namespace API.RedGifs Friend Function UpdateToken() As Boolean Try Dim r$ - Dim NewToken$ = String.Empty + Dim NewToken$ = String.Empty, NewAgent$ = String.Empty Using resp As New Responser : r = resp.GetResponse("https://api.redgifs.com/v2/auth/temporary",, EDP.ThrowException) : End Using If Not r.IsEmptyString Then Dim j As EContainer = JsonDocument.Parse(r) If Not j Is Nothing Then NewToken = j.Value("token") + NewAgent = j.Value("agent") j.Dispose() End If End If If Not NewToken.IsEmptyString Then + If Not NewAgent.IsEmptyString Then UserAgent.Value = NewAgent Token.Value = $"Bearer {NewToken}" TokenLastDateUpdated.Value = Now Return True @@ -118,7 +120,7 @@ Namespace API.RedGifs Return False End If Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "[API.RedGifs.SiteSettings.UpdateToken]", False) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "[API.RedGifs.SiteSettings.UpdateToken]", False) End Try End Function #End Region @@ -129,8 +131,10 @@ Namespace API.RedGifs MyBase.BeginEdit() End Sub Friend Overrides Sub Update() - Dim NewToken$ = AConvert(Of String)(Token.Value, AModes.Var, String.Empty) - If Not _LastTokenValue = NewToken Then TokenLastDateUpdated.Value = Now + If _SiteEditorFormOpened Then + Dim NewToken$ = AConvert(Of String)(Token.Value, AModes.Var, String.Empty) + If Not _LastTokenValue = NewToken Then TokenLastDateUpdated.Value = Now + End If MyBase.Update() End Sub Friend Overrides Sub EndEdit() @@ -141,32 +145,6 @@ Namespace API.RedGifs Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider Return New UserData End Function - Friend Overrides Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable - If BaseAuthExists() Then - Using resp As Responser = Responser.Copy - Dim m As UserMedia = UserData.GetDataFromUrlId(URL, False, resp, Settings(RedGifsSiteKey)) - If Not m.State = UStates.Missing And Not m.State = UserData.DataGone And (m.Type = UTypes.Picture Or m.Type = UTypes.Video) Then - Try - Dim spf$ = String.Empty - Dim f As SFile = GetSpecialDataFile(Path, AskForPath, spf) - If f.IsEmptyString Then - f = m.File.File - Else - f.Name = m.File.Name - f.Extension = m.File.Extension - End If - resp.DownloadFile(m.URL, f, EDP.ThrowException) - m.State = UStates.Downloaded - m.SpecialFolder = spf - Return {m} - Catch ex As Exception - ErrorsDescriber.Execute(EDP.SendInLog, ex, $"Redgifs standalone download error: [{URL}]") - End Try - End If - End Using - End If - Return Nothing - End Function 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 diff --git a/SCrawler/API/Redgifs/UserData.vb b/SCrawler/API/Redgifs/UserData.vb index 64e46f3..db4c4e9 100644 --- a/SCrawler/API/Redgifs/UserData.vb +++ b/SCrawler/API/Redgifs/UserData.vb @@ -9,6 +9,7 @@ Imports System.Net Imports System.Threading Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.Clients @@ -42,7 +43,7 @@ Namespace API.RedGifs Try Dim _page As Func(Of String) = Function() If(Page = 1, String.Empty, $"&page={Page}") URL = $"https://api.redgifs.com/v2/users/{Name}/search?order=recent{_page.Invoke}" - Dim r$ = Responser.GetResponse(URL,, EDP.ThrowException) + Dim r$ = Responser.GetResponse(URL) Dim postDate$, postID$ Dim pTotal% = 0 If Not r.IsEmptyString Then @@ -51,7 +52,7 @@ Namespace API.RedGifs pTotal = j.Value("pages").FromXML(Of Integer)(0) For Each g As EContainer In j("gifs") postDate = g.Value("createDate") - Select Case CheckDatesLimit(postDate, DateProvider) + Select Case CheckDatesLimit(postDate, UnixDate32Provider) Case DateResult.Skip : Continue For Case DateResult.Exit : Exit Sub End Select @@ -106,13 +107,13 @@ Namespace API.RedGifs Dim u As UserMedia Dim j As EContainer For i% = 0 To _ContentList.Count - 1 - If _ContentList(i).State = UserMedia.States.Missing Then + If _ContentList(i).State = UStates.Missing Then ThrowAny(Token) u = _ContentList(i) If Not u.Post.ID.IsEmptyString Then url = String.Format(PostDataUrl, u.Post.ID.ToLower) Try - r = Responser.GetResponse(url,, EDP.ThrowException) + r = Responser.GetResponse(url) If Not r.IsEmptyString Then j = JsonDocument.Parse(r) If Not j Is Nothing Then @@ -207,20 +208,29 @@ Namespace API.RedGifs MyMainLOG = String.Format(_errText, URL) Return m Else - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, String.Format(_errText, URL), m) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, String.Format(_errText, URL), m) End If End If End Try End Function #End Region +#Region "Single data downloader" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim m As UserMedia = GetDataFromUrlId(Data.URL, False, Responser, HOST) + If Not m.State = UStates.Missing And Not m.State = DataGone And (m.Type = UTypes.Picture Or m.Type = UTypes.Video) Then + m.URL_BASE = MySettings.GetUserPostUrl(Me, m) + _TempMediaList.Add(m) + End If + End Sub +#End Region #Region "Create media" - Private Shared Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, - ByVal PostDateStr As String, ByVal PostDateDate As Date?, ByVal State As UStates) As UserMedia + Private Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, + ByVal PostDateStr As String, ByVal PostDateDate As Date?, ByVal State As UStates) As UserMedia _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) Dim m As New UserMedia(_URL, t) With {.Post = New UserPost With {.ID = PostID}} If Not m.URL.IsEmptyString Then m.File = CStr(RegexReplace(m.URL, FilesPattern)) If Not PostDateStr.IsEmptyString Then - m.Post.Date = AConvert(Of Date)(PostDateStr, DateProvider, Nothing) + m.Post.Date = AConvert(Of Date)(PostDateStr, UnixDate32Provider, Nothing) ElseIf PostDateDate.HasValue Then m.Post.Date = PostDateDate Else @@ -233,8 +243,8 @@ Namespace API.RedGifs #Region "Exception" 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 - Dim s As WebExceptionStatus = Responser.Client.Status - Dim sc As HttpStatusCode = Responser.Client.StatusCode + Dim s As WebExceptionStatus = Responser.Status + Dim sc As HttpStatusCode = Responser.StatusCode If sc = HttpStatusCode.NotFound Or s = DataGone Then UserExists = False ElseIf sc = HttpStatusCode.Unauthorized Then diff --git a/SCrawler/API/ThisVid/Declarations.vb b/SCrawler/API/ThisVid/Declarations.vb new file mode 100644 index 0000000..a95bc94 --- /dev/null +++ b/SCrawler/API/ThisVid/Declarations.vb @@ -0,0 +1,22 @@ +' 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.ThisVid + Friend Module Declarations + Friend Const ThisVidSiteKey As String = "AndyProgram_ThisVid" + Friend ReadOnly RegExNextPage As RParams = RParams.DMS("class=.pagination-next...a class=.selective..href=""([^""]+)""", 1) + Friend ReadOnly RegExVideoList As RParams = RParams.DMS("\[\r\n\s]*[\r\n\s]*\ + Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon + Get + Return My.Resources.SiteResources.ThisVidIcon_16 + End Get + End Property + Friend Overrides ReadOnly Property Image As Image + Get + Return My.Resources.SiteResources.ThisVidPic_16 + End Get + End Property + + Friend ReadOnly Property DownloadPublic As PropertyValue + + Friend ReadOnly Property DownloadPrivate As PropertyValue + + Friend ReadOnly Property DifferentFolders As PropertyValue +#End Region +#Region "Initializer" + Friend Sub New() + MyBase.New("ThisVid", "thisvid.com") + DownloadPublic = New PropertyValue(True) + DownloadPrivate = New PropertyValue(True) + DifferentFolders = New PropertyValue(True) + CheckNetscapeCookiesOnEndInit = True + UseNetscapeCookies = True + UserRegex = RParams.DMS("thisvid.com/members/(\d+)", 1) + UrlPatternUser = "https://thisvid.com/members/{0}/" + ImageVideoContains = "https://thisvid.com/videos/" + End Sub +#End Region +#Region "GetInstance, GetSpecialData" + Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider + Return New UserData + End Function +#End Region +#Region "Downloading" + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + Return Settings.YtdlpFile.Exists And (What = ISiteSettings.Download.SingleObject Or Responser.CookiesExists) + End Function +#End Region +#Region "UserOptions" + Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) + If Options Is Nothing OrElse Not TypeOf Options Is UserExchangeOptions Then Options = New UserExchangeOptions(Me) + If OpenForm Then + Using f As New InternalSettingsForm(Options, Me, False) : 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/ThisVid/UserData.vb b/SCrawler/API/ThisVid/UserData.vb new file mode 100644 index 0000000..b7f8aa0 --- /dev/null +++ b/SCrawler/API/ThisVid/UserData.vb @@ -0,0 +1,332 @@ +' Copyright (C) 2023 Andy https://github.com/AAndyProgram +' This program is free software: you can redistribute it and/or modify +' it under the terms of the GNU General Public License as published by +' the Free Software Foundation, either version 3 of the License, or +' (at your option) any later version. +' +' This program is distributed in the hope that it will be useful, +' but WITHOUT ANY WARRANTY +Imports System.Threading +Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools +Imports PersonalUtilities.Tools.Web.Documents.JSON +Namespace API.ThisVid + Friend Class UserData : Inherits UserDataBase +#Region "XML names" + Private Const Name_DownloadPublic As String = "DownloadPublic" + Private Const Name_DownloadPrivate As String = "DownloadPrivate" + Private Const Name_DifferentFolders As String = "DifferentFolders" +#End Region +#Region "Structures" + Private Structure Album : Implements IRegExCreator + Friend URL As String + Friend Title As String + Private Function CreateFromArray(ByVal ParamsArray() As String) As Object Implements IRegExCreator.CreateFromArray + If ParamsArray.ListExists(2) Then + URL = ParamsArray(0) + Title = TitleHtmlConverter(ParamsArray(1)) + End If + Return Me + End Function + End Structure +#End Region +#Region "Declarations" + Friend Property DownloadPublic As Boolean = True + Friend Property DownloadPrivate As Boolean = True + Friend Property DifferentFolders As Boolean = True +#End Region +#Region "Loaders" + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + With Container + If Loading Then + DownloadPublic = .Value(Name_DownloadPublic).FromXML(Of Boolean)(True) + DownloadPrivate = .Value(Name_DownloadPrivate).FromXML(Of Boolean)(True) + DifferentFolders = .Value(Name_DifferentFolders).FromXML(Of Boolean)(True) + Else + .Add(Name_DownloadPublic, DownloadPublic.BoolToInteger) + .Add(Name_DownloadPrivate, DownloadPrivate.BoolToInteger) + .Add(Name_DifferentFolders, DifferentFolders.BoolToInteger) + End If + End With + End Sub + 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) + DownloadPublic = .DownloadPublic + DownloadPrivate = .DownloadPrivate + DifferentFolders = .DifferentFolders + End With + End If + End Sub +#End Region +#Region "Initializer" + Friend Sub New() + UseClientTokens = True + End Sub +#End Region +#Region "Validation" + Private Function IsValid() As Boolean + Const ProfileDataPattern$ = "{0}[\r\n\s\W]*:[\r\n\s\W]*\[\r\n\s\W]*([^\<]*)[\r\n\s\W]*\[\r\n\s\W]*([^\<]*)[\r\n\s\W]*\<" + Try + If Not IsSavedPosts Then + Dim r$ = Responser.GetResponse($"https://thisvid.com/members/{ID}/") + If Not r.IsEmptyString Then + Dim rr As New RParams("", Nothing, 1, EDP.ReturnValue) + Dim __getValue As Func(Of String, Boolean, String) = Function(ByVal member As String, ByVal appendMember As Boolean) As String + rr.Pattern = String.Format(ProfileDataPattern, member) + Dim v$ = CStr(RegexReplace(r, rr)).StringTrim + If Not v.IsEmptyString And appendMember Then v = $"{member}: {v}" + Return v + End Function + UserSiteNameUpdate(__getValue("Name", False)) + If Not UserSiteName.IsEmptyString And FriendlyName.IsEmptyString Then FriendlyName = UserSiteName : _ForceSaveUserData = True + Dim descr$ = String.Empty + descr.StringAppendLine(__getValue("Birth date", True)) + descr.StringAppendLine(__getValue("Country", True)) + descr.StringAppendLine(__getValue("City", True)) + descr.StringAppendLine(__getValue("Gender", True)) + descr.StringAppendLine(__getValue("Orientation", True)) + descr.StringAppendLine(__getValue("Relationship status", True)) + descr.StringAppendLine(__getValue("Favourite category", True)) + descr.StringAppendLine(__getValue("My interests", True)) + rr.Pattern = DescriptionPattern + descr.StringAppendLine(CStr(RegexReplace(r, rr)).StringTrim) + UserDescriptionUpdate(descr) + Else + Return False + End If + End If + Return True + Catch ex As Exception + UserExists = False + Return False + End Try + End Function +#End Region +#Region "Download functions" + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + If ID.IsEmptyString Then ID = Name + If IsValid() Then + If IsSavedPosts Then + DownloadData(1, True, Token) + DownloadData_Images(Token) + Else + If DownloadVideos Then + If DownloadPublic Then DownloadData(1, True, Token) + If DownloadPrivate Then DownloadData(1, False, Token) + End If + If DownloadImages Then DownloadData_Images(Token) + End If + End If + End Sub + Private Overloads Sub DownloadData(ByVal Page As Integer, ByVal IsPublic As Boolean, ByVal Token As CancellationToken) + Dim URL$ = String.Empty + Try + Dim p$ = IIf(Page = 1, String.Empty, $"{Page}/") + If IsSavedPosts Then + URL = $"https://thisvid.com/my_favourite_videos/{p}" + Else + URL = $"https://thisvid.com/members/{ID}/{IIf(IsPublic, "public", "private")}_videos/{p}" + End If + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + Dim cBefore% = _TempMediaList.Count + If Not r.IsEmptyString Then + Dim __SpecialFolder$ = IIf(DifferentFolders, IIf(IsPublic, "Public", "Private"), String.Empty) + Dim l As List(Of String) = RegexReplace(r, If(IsSavedPosts, RegExVideoListSavedPosts, RegExVideoList)) + If l.ListExists Then + For Each u$ In l + If Not u.IsEmptyString Then + If Not _TempPostsList.Contains(u) Then + _TempPostsList.Add(u) + _TempMediaList.Add(New UserMedia(u) With {.Type = UserMedia.Types.VideoPre, .SpecialFolder = __SpecialFolder}) + Else + Exit Sub + End If + End If + Next + End If + End If + If Not cBefore = _TempMediaList.Count Then DownloadData(Page + 1, IsPublic, Token) + Catch ex As Exception + ProcessException(ex, Token, $"videos downloading error [{URL}]") + End Try + End Sub + Private Sub DownloadData_Images(ByVal Token As CancellationToken) + Dim __baseUrl$ = If(IsSavedPosts, "https://thisvid.com/my_favourite_albums/", $"https://thisvid.com/members/{ID}/albums/") + Dim URL$ = String.Empty + Try + Dim r$ + Dim i% = 0 + Dim __continue As Boolean = False + Dim rAlbums As RParams = If(IsSavedPosts, RegExAlbumsListSaved, RegExAlbumsList) + Do + i += 1 + __continue = False + URL = __baseUrl + If i > 1 Then URL &= $"{i}/" + r = Responser.GetResponse(URL) + If Not r.IsEmptyString() Then + Dim albums As List(Of Album) = RegexFields(Of Album)(r, {rAlbums}, {1, 2}, EDP.ReturnValue) + Dim images As List(Of String) + Dim albumId$, img$, imgUrl$, imgId$ + Dim u As UserMedia + Dim rErr As New ErrorsDescriber(EDP.ReturnValue) + __continue = True + If albums.ListExists Then + If albums.Count < 20 Then __continue = False + For Each a As Album In albums + If Not a.URL.IsEmptyString Then + ThrowAny(Token) + r = Responser.GetResponse(a.URL,, rErr) + If Not r.IsEmptyString Then + albumId = RegexReplace(r, RegExAlbumID) + If a.Title.IsEmptyString Then a.Title = albumId + images = RegexReplace(r, RegExAlbumImagesList) + If images.ListExists Then + For Each img In images + ThrowAny(Token) + r = Responser.GetResponse(img,, rErr) + If Not r.IsEmptyString Then + imgUrl = RegexReplace(r, RegExAlbumImageUrl) + If Not imgUrl.IsEmptyString Then + u = New UserMedia(imgUrl) With { + .SpecialFolder = a.Title, + .Type = UserMedia.Types.Picture, + .URL_BASE = img + } + If Not u.File.File.IsEmptyString Then + imgId = $"{albumId}_{u.File.Name}" + If u.File.Extension.IsEmptyString Then u.File.Extension = "jpg" + u.Post = imgId + If Not _TempPostsList.Contains(imgId) Then + _TempPostsList.Add(imgId) + _TempMediaList.Add(u) + Else + Exit For + End If + End If + End If + End If + Next + images.Clear() + End If + End If + End If + Next + Else + Exit Do + End If + End If + Loop While __continue + Catch ex As Exception + ProcessException(ex, Token, $"images downloading error [{URL}]") + End Try + End Sub +#End Region +#Region "ReparseVideo" + Protected Overrides Sub ReparseVideo(ByVal Token As CancellationToken) + Try + If _TempMediaList.Count > 0 Then + Dim u As UserMedia + Dim dirCmd$ = String.Empty + Dim f As SFile = Settings.YtdlpFile.File + Dim n$ + Dim cookieFile As SFile = DirectCast(HOST.Source, SiteSettings).CookiesNetscapeFile + Dim command$ + Dim e As EContainer + For i% = _TempMediaList.Count - 1 To 0 Step -1 + u = _TempMediaList(i) + If u.Type = UserMedia.Types.VideoPre Then + ThrowAny(Token) + command = $"""{f}"" --verbose --dump-json " + If cookieFile.Exists Then command &= $"--no-cookies-from-browser --cookies ""{cookieFile}"" " + command &= u.URL + e = GetJson(command) + If Not e Is Nothing Then + u.URL = e.Value("url") + u.Post = New UserPost(e.Value("id"), ADateTime.ParseUnix32(e.Value("epoch"))) + If u.Post.Date.HasValue Then + Select Case CheckDatesLimit(u.Post.Date.Value, Nothing) + Case DateResult.Skip : _TempPostsList.ListAddValue(u.Post.ID, LNC) : _TempMediaList.RemoveAt(i) : Continue For + Case DateResult.Exit : Exit Sub + End Select + End If + n = TitleHtmlConverter(e.Value("title")) + If Not n.IsEmptyString Then n = n.Replace("ThisVid.com", String.Empty).StringTrim.StringTrimEnd("-").StringTrim + If n.IsEmptyString Then n = u.Post.ID + If n.IsEmptyString Then n = "VideoFile" + u.File = $"{n}.mp4" + If u.URL.IsEmptyString OrElse (Not u.Post.ID.IsEmptyString AndAlso _TempPostsList.Contains(u.Post.ID)) Then + _TempMediaList.RemoveAt(i) + Else + u.Type = UserMedia.Types.Video + _TempPostsList.Add(u.Post.ID) + _TempMediaList(i) = u + End If + e.Dispose() + End If + End If + Next + End If + Catch ex As Exception + ProcessException(ex, Token, "video reparsing error") + End Try + End Sub +#End Region +#Region "GetJson" + Private Function GetJson(ByVal Command As String) As EContainer + Try + Using b As New BatchExecutor(True) + b.Execute(Command, EDP.ReturnValue) + If b.OutputData.Count > 0 Then + Dim e As EContainer + For Each d$ In b.OutputData + If Not d.IsEmptyString AndAlso d.StartsWith("{") Then + e = JsonDocument.Parse(d, EDP.ReturnValue) + If Not e Is Nothing Then Return e + End If + Next + End If + End Using + Return Nothing + Catch ex As Exception + HasError = True + LogError(ex, $"GetJson({Command})") + Return Nothing + End Try + End Function +#End Region +#Region "DownloadContent" + Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) + Dim s As Boolean? = SeparateVideoFolder + If DifferentFolders Then SeparateVideoFolder = False Else SeparateVideoFolder = Nothing + DownloadContentDefault(Token) + SeparateVideoFolder = s + End Sub +#End Region +#Region "Standalone downloader" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + _TempMediaList.Add(New UserMedia(Data.URL) With {.Type = UserMedia.Types.VideoPre}) + ReparseVideo(Token) + End Sub +#End Region +#Region "DownloadingException" + Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, + Optional ByVal EObj As Object = Nothing) As Integer + If Responser.StatusCode = Net.HttpStatusCode.NotFound Then + Return 1 + Else + Return 0 + End If + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/ThisVid/UserExchangeOptions.vb b/SCrawler/API/ThisVid/UserExchangeOptions.vb new file mode 100644 index 0000000..3b91793 --- /dev/null +++ b/SCrawler/API/ThisVid/UserExchangeOptions.vb @@ -0,0 +1,32 @@ +' 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.Plugin.Attributes +Namespace API.ThisVid + Friend Class UserExchangeOptions + + Friend Property DownloadPublic As Boolean = True + + Friend Property DownloadPrivate As Boolean = True + + Friend Property DifferentFolders As Boolean = True + Private ReadOnly Property MySettings As SiteSettings + Friend Sub New(ByVal s As SiteSettings) + DownloadPublic = s.DownloadPublic.Value + DownloadPrivate = s.DownloadPrivate.Value + DifferentFolders = s.DifferentFolders.Value + MySettings = s + End Sub + Friend Sub New(ByVal u As UserData) + DownloadPublic = u.DownloadPublic + DownloadPrivate = u.DownloadPrivate + DifferentFolders = u.DifferentFolders + MySettings = u.HOST.Source + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/TikTok/Declarations.vb b/SCrawler/API/TikTok/Declarations.vb index e749d95..2b030da 100644 --- a/SCrawler/API/TikTok/Declarations.vb +++ b/SCrawler/API/TikTok/Declarations.vb @@ -10,11 +10,7 @@ Imports PersonalUtilities.Functions.RegularExpressions Namespace API.TikTok Friend Module Declarations Friend ReadOnly RegexEnvir As New RegexParseEnvir - Friend ReadOnly CheckDateProvider As New CustomProvider(Function(v, d, p, n, e) - With DirectCast(v, Date?) - If .HasValue Then Return .Value Else Return Nothing - End With - End Function) + Friend ReadOnly CheckDateProvider As New CustomProvider(Function(v) IIf(CType(v, Date?).HasValue, CObj(CType(v, Date?).Value), Nothing)) Friend Class RegexParseEnvir Private ReadOnly UrlIdRegex As RParams = RParams.DMS("http[s]?://[w\.]{0,4}tiktok.com/[^/]+?/video/(\d+)", 1, EDP.ReturnValue) Private ReadOnly RegexItemsArrPre As RParams = RParams.DMS("ItemList"":\{""user-post"":\{""list"":\[([^\[]+)\]", 1) @@ -33,7 +29,7 @@ Namespace API.TikTok End If Return Nothing Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "[API.TikTok.RegexParseEnvir.GetIDList]") + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "[API.TikTok.RegexParseEnvir.GetIDList]") End Try End Function Friend Function GetVideoData(ByVal r As String, ByVal ID As String, ByRef URL As String, ByRef [Date] As Date?) As Boolean @@ -46,12 +42,12 @@ Namespace API.TikTok Dim u$ = RegexReplace(r, VideoPattern) If Not u.IsEmptyString Then URL = SymbolsConverter.Unicode.Decode(u, EDP.ReturnValue) Dim d$ = RegexReplace(r, DatePattern) - If Not d.IsEmptyString Then [Date] = ADateTime.ParseUnicode(d) + If Not d.IsEmptyString Then [Date] = ADateTime.ParseUnix32(d) Return Not URL.IsEmptyString End If Return False Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.SendInLog, ex, "[API.TikTok.RegexParseEnvir.GetVideoData]", False) + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "[API.TikTok.RegexParseEnvir.GetVideoData]", False) End Try End Function Friend Function ExtractPostID(ByVal URL As String) As String diff --git a/SCrawler/API/TikTok/SiteSettings.vb b/SCrawler/API/TikTok/SiteSettings.vb index fd3574e..bf9df98 100644 --- a/SCrawler/API/TikTok/SiteSettings.vb +++ b/SCrawler/API/TikTok/SiteSettings.vb @@ -32,11 +32,12 @@ Namespace API.TikTok Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider Return New UserData End Function - 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 BaseAuthExists() As Boolean Return Responser.CookiesExists End Function + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + 'TODO: TikTok disabled + Return False + End Function End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/TikTok/UserData.vb b/SCrawler/API/TikTok/UserData.vb index fbf1b4d..9ff3590 100644 --- a/SCrawler/API/TikTok/UserData.vb +++ b/SCrawler/API/TikTok/UserData.vb @@ -26,7 +26,7 @@ Namespace API.TikTok Dim PostURL$ = String.Empty Dim r$ URL = $"https://www.tiktok.com/@{Name}" - r = Responser.GetResponse(URL,, EDP.ThrowException) + r = Responser.GetResponse(URL) PostIDs = RegexEnvir.GetIDList(r) If PostIDs.ListExists Then For Each __id$ In PostIDs @@ -52,28 +52,7 @@ Namespace API.TikTok Protected Overrides Sub DownloadContent(ByVal Token As CancellationToken) DownloadContentDefault(Token) End Sub - Friend Shared Function GetVideoInfo(ByVal URL As String, ByVal Responser As Responser, Optional ByVal e As ErrorsDescriber = Nothing) As IEnumerable(Of UserMedia) - Try - If Not URL.IsEmptyString Then - Dim PostId$ = String.Empty - Dim PostDate As Date? = Nothing - Dim PostURL$ = String.Empty - Dim r$ - PostId = RegexEnvir.ExtractPostID(URL) - If Not PostId.IsEmptyString Then - Using resp As Responser = Responser.Copy() : r = resp.GetResponse(URL,, EDP.ThrowException) : End Using - If Not r.IsEmptyString Then - If RegexEnvir.GetVideoData(r, PostId, PostURL, PostDate) Then Return {MediaFromData(PostURL, PostId, PostDate)} - End If - End If - End If - Return Nothing - Catch ex As Exception - If Not e.Exists Then e = New ErrorsDescriber(EDP.ShowMainMsg + EDP.SendInLog) - Return ErrorsDescriber.Execute(e, ex, $"TikTok standalone downloader: fetch media error ({URL})") - End Try - End Function - Private Shared Function MediaFromData(ByVal _URL As String, ByVal PostID As String, ByVal PostDate As Date?) As UserMedia + Private Function MediaFromData(ByVal _URL As String, ByVal PostID As String, ByVal PostDate As Date?) As UserMedia _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) Dim m As New UserMedia(_URL, UserMedia.Types.Video) With {.Post = New UserPost With {.ID = PostID}} If Not m.URL.IsEmptyString Then m.File = $"{PostID}.mp4" diff --git a/SCrawler/API/Twitter/Declarations.vb b/SCrawler/API/Twitter/Declarations.vb index 1073ac4..cee4a04 100644 --- a/SCrawler/API/Twitter/Declarations.vb +++ b/SCrawler/API/Twitter/Declarations.vb @@ -16,7 +16,6 @@ Namespace API.Twitter 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" diff --git a/SCrawler/API/Twitter/EditorExchangeOptions.vb b/SCrawler/API/Twitter/EditorExchangeOptions.vb index 346a41f..6ef40c0 100644 --- a/SCrawler/API/Twitter/EditorExchangeOptions.vb +++ b/SCrawler/API/Twitter/EditorExchangeOptions.vb @@ -6,20 +6,37 @@ ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY +Imports SCrawler.Plugin.Attributes Namespace API.Twitter Friend Class EditorExchangeOptions + Private Const DefaultOffset As Integer = 100 + Friend Property SiteKey As String = TwitterSiteKey + Friend Property GifsDownload As Boolean + Friend Property GifsSpecialFolder As String + Friend Property GifsPrefix As String + Friend Property UseMD5Comparison As Boolean = False + Friend Property RemoveExistingDuplicates As Boolean = False - Friend Sub New() - End Sub + Private ReadOnly Property MySettings As Object Friend Sub New(ByVal s As SiteSettings) GifsDownload = s.GifsDownload.Value GifsSpecialFolder = s.GifsSpecialFolder.Value GifsPrefix = s.GifsPrefix.Value UseMD5Comparison = s.UseMD5Comparison.Value + MySettings = s + End Sub + Friend Sub New(ByVal s As Mastodon.SiteSettings) + GifsDownload = s.GifsDownload.Value + GifsSpecialFolder = s.GifsSpecialFolder.Value + GifsPrefix = s.GifsPrefix.Value + UseMD5Comparison = s.UseMD5Comparison.Value + MySettings = s End Sub Friend Sub New(ByVal u As UserData) GifsDownload = u.GifsDownload @@ -27,6 +44,7 @@ Namespace API.Twitter GifsPrefix = u.GifsPrefix UseMD5Comparison = u.UseMD5Comparison RemoveExistingDuplicates = u.RemoveExistingDuplicates + MySettings = u.HOST.Source End Sub End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Twitter/OptionsForm.Designer.vb b/SCrawler/API/Twitter/OptionsForm.Designer.vb deleted file mode 100644 index 1b2dd82..0000000 --- a/SCrawler/API/Twitter/OptionsForm.Designer.vb +++ /dev/null @@ -1,185 +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 -Namespace API.Twitter - - 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() - Me.components = New System.ComponentModel.Container() - Dim CONTAINER_MAIN As System.Windows.Forms.ToolStripContainer - Dim TP_MAIN As System.Windows.Forms.TableLayoutPanel - Dim ActionButton3 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() - Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(OptionsForm)) - Dim ActionButton4 As PersonalUtilities.Forms.Controls.Base.ActionButton = New PersonalUtilities.Forms.Controls.Base.ActionButton() - Dim TT_MAIN As System.Windows.Forms.ToolTip - Me.CH_DOWN_GIFS = New System.Windows.Forms.CheckBox() - Me.TXT_GIF_FOLDER = New PersonalUtilities.Forms.Controls.TextBoxExtended() - Me.TXT_GIF_PREFIX = New PersonalUtilities.Forms.Controls.TextBoxExtended() - Me.CH_USE_MD5 = New System.Windows.Forms.CheckBox() - Me.CH_REMOVE_EXISTING_DUP = New System.Windows.Forms.CheckBox() - CONTAINER_MAIN = New System.Windows.Forms.ToolStripContainer() - TP_MAIN = New System.Windows.Forms.TableLayoutPanel() - TT_MAIN = New System.Windows.Forms.ToolTip(Me.components) - CONTAINER_MAIN.ContentPanel.SuspendLayout() - CONTAINER_MAIN.SuspendLayout() - TP_MAIN.SuspendLayout() - CType(Me.TXT_GIF_FOLDER, System.ComponentModel.ISupportInitialize).BeginInit() - CType(Me.TXT_GIF_PREFIX, System.ComponentModel.ISupportInitialize).BeginInit() - Me.SuspendLayout() - ' - 'CONTAINER_MAIN - ' - ' - 'CONTAINER_MAIN.ContentPanel - ' - CONTAINER_MAIN.ContentPanel.Controls.Add(TP_MAIN) - CONTAINER_MAIN.ContentPanel.Size = New System.Drawing.Size(304, 161) - 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(304, 161) - 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.TXT_GIF_FOLDER, 0, 1) - TP_MAIN.Controls.Add(Me.TXT_GIF_PREFIX, 0, 2) - TP_MAIN.Controls.Add(Me.CH_USE_MD5, 0, 3) - TP_MAIN.Controls.Add(Me.CH_REMOVE_EXISTING_DUP, 0, 4) - 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 = 6 - 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, 28.0!)) - TP_MAIN.RowStyles.Add(New System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 28.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.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(304, 161) - 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.Padding = New System.Windows.Forms.Padding(100, 0, 0, 0) - Me.CH_DOWN_GIFS.Size = New System.Drawing.Size(296, 19) - Me.CH_DOWN_GIFS.TabIndex = 0 - Me.CH_DOWN_GIFS.Text = "Download GIFs" - Me.CH_DOWN_GIFS.UseVisualStyleBackColor = True - ' - 'TXT_GIF_FOLDER - ' - ActionButton3.BackgroundImage = CType(resources.GetObject("ActionButton3.BackgroundImage"), System.Drawing.Image) - ActionButton3.Name = "Clear" - ActionButton3.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear - Me.TXT_GIF_FOLDER.Buttons.Add(ActionButton3) - Me.TXT_GIF_FOLDER.CaptionText = "GIFs special folder" - Me.TXT_GIF_FOLDER.CaptionToolTipText = "Put the GIFs in a special folder" - Me.TXT_GIF_FOLDER.Dock = System.Windows.Forms.DockStyle.Fill - Me.TXT_GIF_FOLDER.Location = New System.Drawing.Point(4, 30) - Me.TXT_GIF_FOLDER.Name = "TXT_GIF_FOLDER" - Me.TXT_GIF_FOLDER.Size = New System.Drawing.Size(296, 22) - Me.TXT_GIF_FOLDER.TabIndex = 1 - ' - 'TXT_GIF_PREFIX - ' - ActionButton4.BackgroundImage = CType(resources.GetObject("ActionButton4.BackgroundImage"), System.Drawing.Image) - ActionButton4.Name = "Clear" - ActionButton4.Tag = PersonalUtilities.Forms.Controls.Base.ActionButton.DefaultButtons.Clear - Me.TXT_GIF_PREFIX.Buttons.Add(ActionButton4) - Me.TXT_GIF_PREFIX.CaptionText = "GIF prefix" - Me.TXT_GIF_PREFIX.CaptionToolTipText = "This prefix will be added to the beginning of the filename" - Me.TXT_GIF_PREFIX.Dock = System.Windows.Forms.DockStyle.Fill - Me.TXT_GIF_PREFIX.Location = New System.Drawing.Point(4, 59) - Me.TXT_GIF_PREFIX.Name = "TXT_GIF_PREFIX" - Me.TXT_GIF_PREFIX.Size = New System.Drawing.Size(296, 22) - Me.TXT_GIF_PREFIX.TabIndex = 2 - ' - 'CH_USE_MD5 - ' - Me.CH_USE_MD5.AutoSize = True - Me.CH_USE_MD5.Dock = System.Windows.Forms.DockStyle.Fill - Me.CH_USE_MD5.Location = New System.Drawing.Point(4, 88) - Me.CH_USE_MD5.Name = "CH_USE_MD5" - Me.CH_USE_MD5.Padding = New System.Windows.Forms.Padding(100, 0, 0, 0) - Me.CH_USE_MD5.Size = New System.Drawing.Size(296, 19) - Me.CH_USE_MD5.TabIndex = 3 - Me.CH_USE_MD5.Text = "Use MD5 comparison" - TT_MAIN.SetToolTip(Me.CH_USE_MD5, "Each image will be checked for existence using MD5") - Me.CH_USE_MD5.UseVisualStyleBackColor = True - ' - 'CH_REMOVE_EXISTING_DUP - ' - Me.CH_REMOVE_EXISTING_DUP.AutoSize = True - Me.CH_REMOVE_EXISTING_DUP.Dock = System.Windows.Forms.DockStyle.Fill - Me.CH_REMOVE_EXISTING_DUP.Location = New System.Drawing.Point(4, 114) - Me.CH_REMOVE_EXISTING_DUP.Name = "CH_REMOVE_EXISTING_DUP" - Me.CH_REMOVE_EXISTING_DUP.Padding = New System.Windows.Forms.Padding(100, 0, 0, 0) - Me.CH_REMOVE_EXISTING_DUP.Size = New System.Drawing.Size(296, 19) - Me.CH_REMOVE_EXISTING_DUP.TabIndex = 4 - Me.CH_REMOVE_EXISTING_DUP.Text = "Remove existing duplicates" - TT_MAIN.SetToolTip(Me.CH_REMOVE_EXISTING_DUP, "Existing files will be checked for duplicates and duplicates removed." & Global.Microsoft.VisualBasic.ChrW(13) & Global.Microsoft.VisualBasic.ChrW(10) & "Works only" & - " on the first activation 'Use MD5 comparison'.") - Me.CH_REMOVE_EXISTING_DUP.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(304, 161) - Me.Controls.Add(CONTAINER_MAIN) - Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle - Me.Icon = Global.SCrawler.My.Resources.SiteResources.TwitterIcon_32 - Me.MaximizeBox = False - Me.MaximumSize = New System.Drawing.Size(320, 200) - Me.MinimizeBox = False - Me.MinimumSize = New System.Drawing.Size(320, 200) - Me.Name = "OptionsForm" - Me.ShowInTaskbar = False - 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() - CType(Me.TXT_GIF_FOLDER, System.ComponentModel.ISupportInitialize).EndInit() - CType(Me.TXT_GIF_PREFIX, System.ComponentModel.ISupportInitialize).EndInit() - Me.ResumeLayout(False) - - End Sub - Private WithEvents CH_DOWN_GIFS As CheckBox - Private WithEvents TXT_GIF_FOLDER As PersonalUtilities.Forms.Controls.TextBoxExtended - Private WithEvents TXT_GIF_PREFIX As PersonalUtilities.Forms.Controls.TextBoxExtended - Private WithEvents CH_USE_MD5 As CheckBox - Private WithEvents CH_REMOVE_EXISTING_DUP As CheckBox - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/Twitter/OptionsForm.vb b/SCrawler/API/Twitter/OptionsForm.vb deleted file mode 100644 index 8cac46e..0000000 --- a/SCrawler/API/Twitter/OptionsForm.vb +++ /dev/null @@ -1,81 +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 SCrawler.Plugin.Attributes -Imports PersonalUtilities.Forms -Imports PersonalUtilities.Forms.Controls -Namespace API.Twitter - Friend Class OptionsForm - Private WithEvents MyDefs As DefaultFormOptions - Private ReadOnly Property MyExchangeOptions As EditorExchangeOptions - Private ReadOnly MyGifTextProvider As SiteSettings.GifStringProvider - Friend Sub New(ByRef ExchangeOptions As EditorExchangeOptions) - InitializeComponent() - MyExchangeOptions = ExchangeOptions - MyGifTextProvider = New SiteSettings.GifStringProvider - MyDefs = New DefaultFormOptions(Me, Settings.Design) - End Sub - Private Sub OptionsForm_Load(sender As Object, e As EventArgs) Handles Me.Load - With MyDefs - .MyViewInitialize(True) - .AddOkCancelToolbar() - With MyExchangeOptions - CH_DOWN_GIFS.Checked = .GifsDownload - TXT_GIF_FOLDER.Text = .GifsSpecialFolder - TXT_GIF_FOLDER.Tag = NameOf(SiteSettings.GifsSpecialFolder) - TXT_GIF_PREFIX.Text = .GifsPrefix - TXT_GIF_PREFIX.Tag = NameOf(SiteSettings.GifsPrefix) - CH_USE_MD5.Checked = .UseMD5Comparison - CH_REMOVE_EXISTING_DUP.Checked = .RemoveExistingDuplicates - - Try - Dim p As PropertyOption - With Settings(TwitterSiteKey) - p = .PropList.Find(Function(pp) pp.Name = TXT_GIF_FOLDER.Tag).Options - If Not p Is Nothing Then - TXT_GIF_FOLDER.CaptionText = p.ControlText - TXT_GIF_FOLDER.CaptionToolTipText = p.ControlToolTip - TXT_GIF_FOLDER.CaptionToolTipEnabled = Not TXT_GIF_FOLDER.CaptionToolTipText.IsEmptyString - End If - - p = .PropList.Find(Function(pp) pp.Name = TXT_GIF_PREFIX.Tag).Options - If Not p Is Nothing Then - TXT_GIF_PREFIX.CaptionText = p.ControlText - TXT_GIF_PREFIX.CaptionToolTipText = p.ControlToolTip - TXT_GIF_PREFIX.CaptionToolTipEnabled = Not TXT_GIF_PREFIX.CaptionToolTipText.IsEmptyString - End If - End With - Catch - End Try - End With - .EndLoaderOperations() - End With - End Sub - Private Sub MyDefs_ButtonOkClick(ByVal Sender As Object, ByVal e As KeyHandleEventArgs) Handles MyDefs.ButtonOkClick - With MyExchangeOptions - .GifsDownload = CH_DOWN_GIFS.Checked - .GifsSpecialFolder = TXT_GIF_FOLDER.Text - .GifsPrefix = TXT_GIF_PREFIX.Text - .UseMD5Comparison = CH_USE_MD5.Checked - .RemoveExistingDuplicates = CH_REMOVE_EXISTING_DUP.Checked - End With - MyDefs.CloseForm() - End Sub - Private Sub TXT_ActionOnTextChanged(ByVal Sender As TextBoxExtended, ByVal e As EventArgs) Handles TXT_GIF_FOLDER.ActionOnTextChanged, - TXT_GIF_PREFIX.ActionOnTextChanged - If Not MyDefs.Initializing Then - With Sender - MyGifTextProvider.PropertyName = .Tag - Dim s% = .SelectionStart - Dim t$ = AConvert(Of String)(.Text, String.Empty, MyGifTextProvider) - If Not .Text = t Then .Text = t : .Select(s, 0) - End With - End If - End Sub - End Class -End Namespace \ No newline at end of file diff --git a/SCrawler/API/Twitter/SiteSettings.vb b/SCrawler/API/Twitter/SiteSettings.vb index c5d2043..358f89b 100644 --- a/SCrawler/API/Twitter/SiteSettings.vb +++ b/SCrawler/API/Twitter/SiteSettings.vb @@ -11,12 +11,25 @@ Imports SCrawler.Plugin Imports SCrawler.Plugin.Attributes Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.Clients -Imports PersonalUtilities.Tools.Web.Cookies Namespace API.Twitter Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Token names" Friend Const Header_Authorization As String = "authorization" Friend Const Header_Token As String = "x-csrf-token" +#End Region +#Region "Properties constants" + Friend Const GifsSpecialFolder_Text As String = "GIFs special folder" + Friend Const GifsSpecialFolder_ToolTip As String = "Put the GIFs in a special folder" & vbCr & + "This is a folder name, not an absolute path." & vbCr & + "This folder(s) will be created relative to the user's root folder." & vbCr & + "Examples:" & vbCr & "SomeFolderName" & vbCr & "SomeFolderName\SomeFolderName2" + Friend Const GifsPrefix_Text As String = "GIF prefix" + Friend Const GifsPrefix_ToolTip As String = "This prefix will be added to the beginning of the filename" + Friend Const GifsDownload_Text As String = "Download GIFs" + Friend Const UseMD5Comparison_Text As String = "Use MD5 comparison" + Friend Const UseMD5Comparison_ToolTip As String = "Each image will be checked for existence using MD5" +#End Region #Region "Declarations" Friend Overrides ReadOnly Property Icon As Icon Get @@ -34,19 +47,13 @@ Namespace API.Twitter Private ReadOnly Property Auth As PropertyValue Private ReadOnly Property Token As PropertyValue - - Friend ReadOnly Property SavedPostsUserName As PropertyValue #End Region #Region "Other properties" - + Friend ReadOnly Property GifsDownload As PropertyValue - + Friend ReadOnly Property GifsSpecialFolder As PropertyValue - + Friend ReadOnly Property GifsPrefix As PropertyValue Private ReadOnly Property GifStringChecker As IFormatProvider @@ -60,69 +67,18 @@ Namespace API.Twitter v = v.StringRemoveWinForbiddenSymbols Else v = v.StringReplaceSymbols(GetWinForbiddenSymbols.ToList.ListWithRemove("\").ToArray, String.Empty, EDP.ReturnValue) - v = v.StringTrim("\") End If End If Return v End Function Private Function GetFormat(ByVal FormatType As Type) As Object Implements IFormatProvider.GetFormat - Throw New NotImplementedException("[GetFormat] is not available in the context of [TimersChecker]") + Throw New NotImplementedException("[GetFormat] is not available in the context of [GifStringProvider]") End Function End Class - + Friend ReadOnly Property UseMD5Comparison As PropertyValue #End Region Friend Overrides ReadOnly Property Responser As Responser -#End Region - Friend Sub New() - MyBase.New(TwitterSite) - Responser = New Responser($"{SettingsFolderName}\Responser_{Site}.xml") - - Dim a$ = String.Empty - Dim t$ = String.Empty - - With Responser - If .File.Exists Then - Dim b As Boolean = .CookiesDomain.IsEmptyString - If EncryptCookies.CookiesEncrypted Then .CookiesEncryptKey = SettingsCLS.CookieEncryptKey - .LoadSettings() - a = .Headers.Value(Header_Authorization) - t = .Headers.Value(Header_Token) - .CookiesDomain = "twitter.com" - If b Then .SaveSettings() - Else - .ContentType = "application/json" - .Accept = "*/*" - .CookiesDomain = "twitter.com" - .CookiesEncryptKey = SettingsCLS.CookieEncryptKey - .Decoders.Add(SymbolsConverter.Converters.Unicode) - .Headers.Add("sec-ch-ua", " Not;A Brand"";v=""99"", ""Google Chrome"";v=""91"", ""Chromium"";v=""91""") - .Headers.Add("sec-ch-ua-mobile", "?0") - .Headers.Add("sec-fetch-dest", "empty") - .Headers.Add("sec-fetch-mode", "cors") - .Headers.Add("sec-fetch-site", "same-origin") - .Headers.Add(Header_Token, String.Empty) - .Headers.Add("x-twitter-active-user", "yes") - .Headers.Add("x-twitter-auth-type", "OAuth2Session") - .Headers.Add(Header_Authorization, String.Empty) - .SaveSettings() - End If - End With - - Auth = New PropertyValue(a, GetType(String), Sub(v) ChangeResponserFields(NameOf(Auth), v)) - Token = New PropertyValue(t, GetType(String), Sub(v) ChangeResponserFields(NameOf(Token), v)) - SavedPostsUserName = New PropertyValue(String.Empty, GetType(String)) - - GifsDownload = New PropertyValue(True) - GifsSpecialFolder = New PropertyValue(String.Empty, GetType(String)) - GifsPrefix = New PropertyValue("GIF_") - GifStringChecker = New GifStringProvider - UseMD5Comparison = New PropertyValue(False) - - UserRegex = RParams.DMS("[htps:/]{7,8}.*?twitter.com/([^/]+)", 1) - UrlPatternUser = "https://twitter.com/{0}" - ImageVideoContains = "twitter" - End Sub Private Sub ChangeResponserFields(ByVal PropName As String, ByVal Value As Object) If Not PropName.IsEmptyString Then Dim f$ = String.Empty @@ -137,15 +93,59 @@ Namespace API.Twitter End If End If End Sub +#End Region + Friend Sub New() + MyBase.New(TwitterSite) + Responser = New Responser($"{SettingsFolderName}\Responser_{Site}.xml") With {.DeclaredError = EDP.ThrowException} + + Dim a$ = String.Empty + Dim t$ = String.Empty + + With Responser + If .File.Exists Then + .CookiesDomain = "twitter.com" + .CookiesEncryptKey = SettingsCLS.CookieEncryptKey + .LoadSettings() + a = .Headers.Value(Header_Authorization) + t = .Headers.Value(Header_Token) + Else + .ContentType = "application/json" + .Accept = "*/*" + .CookiesDomain = "twitter.com" + .CookiesEncryptKey = SettingsCLS.CookieEncryptKey + .Decoders.Add(SymbolsConverter.Converters.Unicode) + .Headers.Add("sec-ch-ua", """Chromium"";v=""112"", ""Google Chrome"";v=""112"", ""Not:A-Brand"";v=""99""") + .Headers.Add("sec-ch-ua-mobile", "?0") + .Headers.Add("sec-fetch-dest", "empty") + .Headers.Add("sec-fetch-mode", "cors") + .Headers.Add("sec-fetch-site", "same-origin") + .Headers.Add(Header_Token, String.Empty) + .Headers.Add("x-twitter-active-user", "yes") + .Headers.Add("x-twitter-auth-type", "OAuth2Session") + .Headers.Add(Header_Authorization, String.Empty) + .SaveSettings() + End If + .Cookies.ChangedAllowInternalDrop = False + .Cookies.Changed = False + End With + + Auth = New PropertyValue(a, GetType(String), Sub(v) ChangeResponserFields(NameOf(Auth), v)) + Token = New PropertyValue(t, GetType(String), Sub(v) ChangeResponserFields(NameOf(Token), v)) + + GifsDownload = New PropertyValue(True) + GifsSpecialFolder = New PropertyValue(String.Empty, GetType(String)) + GifsPrefix = New PropertyValue("GIF_") + GifStringChecker = New GifStringProvider + UseMD5Comparison = New PropertyValue(False) + + UserRegex = RParams.DMS("[htps:/]{7,8}.*?twitter.com/([^/]+)", 1) + UrlPatternUser = "https://twitter.com/{0}" + ImageVideoContains = "twitter" + CheckNetscapeCookiesOnEndInit = True + UseNetscapeCookies = True + End Sub Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider - If What = ISiteSettings.Download.SavedPosts Then - Return New UserData With {.IsSavedPosts = True, .User = New UserInfo With {.Name = 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 - Return UserData.GetVideoInfo(URL, Responser) + Return New UserData End Function Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String Return $"https://twitter.com/{User.Name}/status/{Media.Post.ID}" @@ -153,11 +153,31 @@ Namespace API.Twitter Friend Overrides Function BaseAuthExists() As Boolean Return Responser.CookiesExists And ACheck(Token.Value) And ACheck(Auth.Value) End Function - Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) - If Options Is Nothing OrElse Not TypeOf Options Is EditorExchangeOptions Then Options = New EditorExchangeOptions(Me) - If OpenForm Then - Using f As New OptionsForm(Options) : f.ShowDialog() : End Using + Friend Overrides Function Available(ByVal What As ISiteSettings.Download, ByVal Silent As Boolean) As Boolean + If MyBase.Available(What, Silent) Then + If What = ISiteSettings.Download.SavedPosts Then + Return Settings.GalleryDLFile.Exists + Else + Return True + End If + Else + Return False End If + End Function + Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) + If Options Is Nothing OrElse (Not TypeOf Options Is EditorExchangeOptions OrElse + Not DirectCast(Options, EditorExchangeOptions).SiteKey = TwitterSiteKey) Then _ + Options = New EditorExchangeOptions(Me) + If OpenForm Then + Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using + End If + End Sub + Friend Overrides Sub Update() + If _SiteEditorFormOpened Then + Dim tf$ = GifsSpecialFolder.Value + If Not tf.IsEmptyString Then tf = tf.StringTrim("\") : GifsSpecialFolder.Value = tf + End If + MyBase.Update() End Sub End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Twitter/UserData.vb b/SCrawler/API/Twitter/UserData.vb index 855bf6c..069d567 100644 --- a/SCrawler/API/Twitter/UserData.vb +++ b/SCrawler/API/Twitter/UserData.vb @@ -7,39 +7,32 @@ ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY Imports System.Net -Imports System.Drawing Imports System.Threading Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.Clients Imports PersonalUtilities.Tools.Web.Documents.JSON -Imports PersonalUtilities.Tools.ImageRenderer Imports UStates = SCrawler.API.Base.UserMedia.States Imports UTypes = SCrawler.API.Base.UserMedia.Types Namespace API.Twitter Friend Class UserData : Inherits UserDataBase - Private Const SinglePostUrl As String = "https://api.twitter.com/1.1/statuses/show.json?id={0}&tweet_mode=extended" + Protected SinglePostUrl As String = "https://api.twitter.com/1.1/statuses/show.json?id={0}&tweet_mode=extended" #Region "XML names" Private Const Name_GifsDownload As String = "GifsDownload" Private Const Name_GifsSpecialFolder As String = "GifsSpecialFolder" Private Const Name_GifsPrefix As String = "GifsPrefix" - Private Const Name_UseMD5Comparison As String = "UseMD5Comparison" - Private Const Name_RemoveExistingDuplicates As String = "RemoveExistingDuplicates" - Private Const Name_StartMD5Checked As String = "StartMD5Checked" #End Region #Region "Declarations" - Friend Property GifsDownload As Boolean - Friend Property GifsSpecialFolder As String - Friend Property GifsPrefix As String + Friend Property GifsDownload As Boolean = True + Friend Property GifsSpecialFolder As String = String.Empty + Friend Property GifsPrefix As String = String.Empty Private ReadOnly _DataNames As List(Of String) - Friend Property UseMD5Comparison As Boolean = False - Private StartMD5Checked As Boolean = False - Friend Property RemoveExistingDuplicates As Boolean = False #End Region #Region "Exchange options" Friend Overrides Function ExchangeOptionsGet() As Object - Return New EditorExchangeOptions(Me) + Return New EditorExchangeOptions(Me) With {.SiteKey = HOST.Key} End Function Friend Overrides Sub ExchangeOptionsSet(ByVal Obj As Object) If Not Obj Is Nothing AndAlso TypeOf Obj Is EditorExchangeOptions Then @@ -83,45 +76,35 @@ Namespace API.Twitter Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) If IsSavedPosts Then If _ContentList.Count > 0 Then _DataNames.ListAddList(_ContentList.Select(Function(c) c.Post.ID), LAP.ClearBeforeAdd, LAP.NotContainsOnly) - DownloadData(String.Empty, Token) + DownloadData_SavedPosts(Token) Else If _ContentList.Count > 0 Then _DataNames.ListAddList(_ContentList.Select(Function(c) c.File.File), LAP.ClearBeforeAdd, LAP.NotContainsOnly) DownloadData(String.Empty, Token) - If UseMD5Comparison Then ValidateMD5(Token) End If End Sub Private Overloads Sub DownloadData(ByVal POST As String, ByVal Token As CancellationToken) Dim URL$ = String.Empty Try - Dim NextCursor$ = String.Empty - Dim __NextCursor As Predicate(Of EContainer) = Function(e) e.Value({"content", "operation", "cursor"}, "cursorType") = "Bottom" Dim PostID$ = String.Empty Dim PostDate$ - Dim nn As EContainer, s As EContainer + Dim nn As EContainer Dim NewPostDetected As Boolean = False Dim ExistsDetected As Boolean = False Dim UID As Func(Of EContainer, String) = Function(e) e.XmlIfNothing.Item({"user", "id"}).XmlIfNothingValue - If IsSavedPosts Then - If Name.IsEmptyString Then Throw New ArgumentNullException With {.HelpLink = 1} - URL = $"https://api.twitter.com/2/timeline/bookmark.json?screen_name={Name}&count=200" & - "&tweet_mode=extended&include_entities=true&include_user_entities=true&include_ext_media_availability=true" - If Not POST.IsEmptyString Then URL &= $"&cursor={SymbolsConverter.ASCII.EncodeSymbolsOnly(POST)}" - Else - URL = $"https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={Name}&count=200&exclude_replies=false&include_rts=1&tweet_mode=extended" - If Not POST.IsEmptyString Then URL &= $"&max_id={POST}" - End If + URL = $"https://api.twitter.com/1.1/statuses/user_timeline.json?screen_name={Name}&count=200&exclude_replies=false&include_rts=1&tweet_mode=extended" + If Not POST.IsEmptyString Then URL &= $"&max_id={POST}" ThrowAny(Token) - Dim r$ = Responser.GetResponse(URL,, EDP.ThrowException) + Dim r$ = Responser.GetResponse(URL) If Not r.IsEmptyString Then Using w As EContainer = JsonDocument.Parse(r) If w.ListExists Then - If Not IsSavedPosts And POST.IsEmptyString And Not w.ItemF({0, "user"}) Is Nothing Then + If POST.IsEmptyString And Not w.ItemF({0, "user"}) Is Nothing Then With w.ItemF({0, "user"}) - If .Value("screen_name").StringToLower = Name Then + If .Value("screen_name").StringToLower = Name.ToLower Then UserSiteNameUpdate(.Value("name")) UserDescriptionUpdate(.Value("description")) Dim __getImage As Action(Of String) = Sub(ByVal img As String) @@ -145,15 +128,10 @@ Namespace API.Twitter For Each nn In If(IsSavedPosts, w({"globalObjects", "tweets"}).XmlIfNothing, w) ThrowAny(Token) If nn.Count > 0 Then - If IsSavedPosts Then - PostID = nn.Value - If PostID.IsEmptyString Then PostID = nn.Value("id_str") - Else - PostID = nn.Value("id") - If ID.IsEmptyString Then - ID = UID(nn) - If Not ID.IsEmptyString Then UpdateUserInformation() - End If + PostID = nn.Value("id") + If ID.IsEmptyString Then + ID = UID(nn) + If Not ID.IsEmptyString Then UpdateUserInformation() End If 'Date Pattern: @@ -172,32 +150,58 @@ Namespace API.Twitter Continue For End If - If IsSavedPosts OrElse Not ParseUserMediaOnly OrElse - ( - Not nn.Contains("retweeted_status") OrElse - (Not ID.IsEmptyString AndAlso UID(nn("retweeted_status")) = ID) - ) Then ObtainMedia(nn, PostID, PostDate) + If Not ParseUserMediaOnly OrElse + (Not nn.Contains("retweeted_status") OrElse (Not ID.IsEmptyString AndAlso UID(nn("retweeted_status")) = ID)) Then _ + ObtainMedia(nn, PostID, PostDate) End If Next - - If IsSavedPosts Then - s = w.ItemF({"timeline", "instructions", 0, "addEntries", "entries"}).XmlIfNothing - If s.Count > 0 Then NextCursor = If(s.ItemF({__NextCursor})?.Value({"content", "operation", "cursor"}, "value"), String.Empty) - End If End If End Using - If IsSavedPosts Then - If Not NextCursor.IsEmptyString And Not NextCursor = POST Then DownloadData(NextCursor, Token) - Else - If POST.IsEmptyString And ExistsDetected Then Exit Sub - If Not PostID.IsEmptyString And NewPostDetected Then DownloadData(PostID, Token) + If POST.IsEmptyString And ExistsDetected Then Exit Sub + If Not PostID.IsEmptyString And NewPostDetected Then DownloadData(PostID, Token) + End If + Catch ex As Exception + ProcessException(ex, Token, $"data downloading error [{URL}]") + End Try + End Sub + Private Sub DownloadData_SavedPosts(ByVal Token As CancellationToken) + Try + Dim urls As List(Of String) = GetBookmarksUrlsFromGalleryDL() + If urls.ListExists Then + Dim postIds As New List(Of String) + Dim r$ + Dim j As EContainer, jj As EContainer + Dim jErr As New ErrorsDescriber(EDP.ReturnValue) + Dim rPattern As RParams = RParams.DM("(?<=tweet-)(\d+)\Z", 0, EDP.ReturnValue) + For Each url$ In urls + r = Responser.GetResponse(url) + If Not r.IsEmptyString Then + j = JsonDocument.Parse(r, jErr) + If Not j Is Nothing Then + jj = j.ItemF({"data", "bookmark_timeline_v2", "timeline", "instructions", 0, "entries"}) + If If(jj?.Count, 0) > 0 Then postIds.ListAddList(jj.Select(Function(jj2) CStr(RegexReplace(jj2.Value("entryId"), rPattern))), LNC) + j.Dispose() + End If + End If + Next + If postIds.Count > 0 Then postIds.RemoveAll(Function(pid) pid.IsEmptyString OrElse (_TempPostsList.Contains(pid) Or _DataNames.Contains(pid))) + If postIds.Count > 0 Then + For Each __id$ In postIds + _TempPostsList.Add(__id) + r = Responser.GetResponse(String.Format(SinglePostUrl, __id),, EDP.ReturnValue) + If Not r.IsEmptyString Then + j = JsonDocument.Parse(r, jErr) + If Not j Is Nothing Then + If j.Count > 0 Then ObtainMedia(j, __id, j.Value("created_at")) + j.Dispose() + End If + End If + Next End If End If - Catch ane As ArgumentNullException When ane.HelpLink = 1 - MyMainLOG = "Username not set for saved Twitter posts" Catch ex As Exception - ProcessException(ex, Token, $"data downloading error{IIf(IsSavedPosts, " (Saved Posts)", String.Empty)} [{URL}]") + ProcessException(ex, Token, "data downloading error (Saved Posts)") End Try End Sub #End Region @@ -252,18 +256,24 @@ Namespace API.Twitter If .ListExists Then For Each n As EContainer In .Self If n.Value("type") = "animated_gif" Then - With n({"video_info", "variants"}).XmlIfNothing.ItemF({gifUrl}).XmlIfNothing - url = .Value("url") - ff = UrlFile(url) - If Not ff.IsEmptyString Then - If GifsDownload And Not _DataNames.Contains(ff) Then - m = MediaFromData(url, PostID, PostDate,, State, UTypes.Video) - f = m.File - If Not f.IsEmptyString And Not GifsPrefix.IsEmptyString Then f.Name = $"{GifsPrefix}{f.Name}" : m.File = f - If Not GifsSpecialFolder.IsEmptyString Then m.SpecialFolder = $"{GifsSpecialFolder}*" - _TempMediaList.ListAddValue(m, LNC) - End If - Return True + With n({"video_info", "variants"}) + If .ListExists Then + With .ItemF({gifUrl}) + If .ListExists Then + url = .Value("url") + ff = UrlFile(url) + If Not ff.IsEmptyString Then + If GifsDownload And Not _DataNames.Contains(ff) Then + m = MediaFromData(url, PostID, PostDate,, State, UTypes.Video) + f = m.File + If Not f.IsEmptyString And Not GifsPrefix.IsEmptyString Then f.Name = $"{GifsPrefix}{f.Name}" : m.File = f + If Not GifsSpecialFolder.IsEmptyString Then m.SpecialFolder = $"{GifsSpecialFolder}*" + _TempMediaList.ListAddValue(m, LNC) + End If + Return True + End If + End If + End With End If End With End If @@ -276,7 +286,7 @@ Namespace API.Twitter Return False End Try End Function - Private Shared Function GetVideoNodeURL(ByVal w As EContainer) As String + Private Function GetVideoNodeURL(ByVal w As EContainer) As String Dim v As EContainer = w.GetNode(VideoNode) If v.ListExists Then Dim l As New List(Of Sizes) @@ -298,6 +308,18 @@ Namespace API.Twitter Return String.Empty End Function #End Region +#Region "Gallery-DL Support" + Private Function GetBookmarksUrlsFromGalleryDL() As List(Of String) + Dim command$ = $"gallery-dl --verbose --simulate --cookies ""{DirectCast(HOST.Source, SiteSettings).CookiesNetscapeFile}"" https://twitter.com/i/bookmarks" + Try + Using batch As New GDL.GDLBatch With {.TempPostsList = _TempPostsList} : Return GDL.GetUrlsFromGalleryDl(batch, command) : End Using + Catch ex As Exception + HasError = True + LogError(ex, $"GetJson({command})") + Return Nothing + End Try + End Function +#End Region #Region "ReparseMissing" Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken) Dim rList As New List(Of Integer) @@ -337,156 +359,19 @@ Namespace API.Twitter End Try End Sub #End Region -#Region "MD5 support" - Private Const VALIDATE_MD5_ERROR As String = "VALIDATE_MD5_ERROR" - Private Sub ValidateMD5(ByVal Token As CancellationToken) - Try - Dim missingMD5 As Predicate(Of UserMedia) = Function(d) (d.Type = UTypes.GIF Or d.Type = UTypes.Picture) And d.MD5.IsEmptyString - If UseMD5Comparison And _TempMediaList.Exists(missingMD5) Then - Dim i% - Dim data As UserMedia = Nothing - Dim hashList As New Dictionary(Of String, SFile) - Dim f As SFile - Dim ErrMD5 As New ErrorsDescriber(EDP.ReturnValue) - Dim __getMD5 As Func(Of UserMedia, Boolean, String) = - Function(ByVal __data As UserMedia, ByVal IsUrl As Boolean) As String - Try - Dim ImgFormat As Imaging.ImageFormat = Nothing - Dim hash$ = String.Empty - Dim __isGif As Boolean = False - If __data.Type = UTypes.GIF Then - ImgFormat = Imaging.ImageFormat.Gif - __isGif = True - ElseIf Not __data.File.IsEmptyString Then - ImgFormat = GetImageFormat(__data.File) - End If - If ImgFormat Is Nothing Then ImgFormat = Imaging.ImageFormat.Jpeg - If IsUrl Then - hash = ByteArrayToString(GetMD5(SFile.GetBytesFromNet(__data.URL_BASE.IfNullOrEmpty(__data.URL), ErrMD5), ImgFormat, ErrMD5)) - Else - hash = ByteArrayToString(GetMD5(SFile.GetBytes(__data.File, ErrMD5), ImgFormat, ErrMD5)) - End If - If hash.IsEmptyString And Not __isGif Then - If ImgFormat Is Imaging.ImageFormat.Jpeg Then ImgFormat = Imaging.ImageFormat.Png Else ImgFormat = Imaging.ImageFormat.Jpeg - If IsUrl Then - hash = ByteArrayToString(GetMD5(SFile.GetBytesFromNet(__data.URL_BASE.IfNullOrEmpty(__data.URL), ErrMD5), ImgFormat, ErrMD5)) - Else - hash = ByteArrayToString(GetMD5(SFile.GetBytes(__data.File, ErrMD5), ImgFormat, ErrMD5)) - End If - End If - Return hash - Catch - Return String.Empty - End Try - End Function - If Not StartMD5Checked Then - StartMD5Checked = True - If _ContentList.Exists(missingMD5) Then - Dim existingFiles As List(Of SFile) = SFile.GetFiles(MyFileSettings.CutPath, "*.jpg|*.jpeg|*.png|*.gif",, EDP.ReturnValue).ListIfNothing - Dim eIndx% - Dim eFinder As Predicate(Of SFile) = Function(ff) ff.File = data.File.File - If RemoveExistingDuplicates Then - RemoveExistingDuplicates = False - _ForceSaveUserInfo = True - If existingFiles.Count > 0 Then - Dim h$ - For i = existingFiles.Count - 1 To 0 Step -1 - h = __getMD5(New UserMedia With {.File = existingFiles(i)}, False) - If Not h.IsEmptyString Then - If hashList.ContainsKey(h) Then - MyMainLOG = $"{ToStringForLog()}: Removed image [{existingFiles(i).File}] (duplicate of [{hashList(h).File}])" - existingFiles(i).Delete(SFO.File, SFODelete.DeleteToRecycleBin, ErrMD5) - existingFiles.RemoveAt(i) - Else - hashList.Add(h, existingFiles(i)) - End If - End If - Next - End If - End If - For i = 0 To _ContentList.Count - 1 - data = _ContentList(i) - If (data.Type = UTypes.GIF Or data.Type = UTypes.Picture) Then - If data.MD5.IsEmptyString Then - ThrowAny(Token) - eIndx = existingFiles.FindIndex(eFinder) - If eIndx >= 0 Then - data.MD5 = __getMD5(New UserMedia With {.File = existingFiles(eIndx)}, False) - If Not data.MD5.IsEmptyString Then _ContentList(i) = data : _ForceSaveUserData = True - End If - End If - existingFiles.RemoveAll(eFinder) - End If - Next - If existingFiles.Count > 0 Then - For i = 0 To existingFiles.Count - 1 - f = existingFiles(i) - data = New UserMedia(f.File) With { - .State = UStates.Downloaded, - .Type = IIf(f.Extension = "gif", UTypes.GIF, UTypes.Picture), - .File = f - } - ThrowAny(Token) - data.MD5 = __getMD5(data, False) - If Not data.MD5.IsEmptyString Then _ContentList.Add(data) : _ForceSaveUserData = True - Next - existingFiles.Clear() - End If - End If - End If - - If _ContentList.Count > 0 Then - With _ContentList.Select(Function(d) d.MD5) - If .ListExists Then .ToList.ForEach(Sub(md5value) _ - If Not md5value.IsEmptyString AndAlso Not hashList.ContainsKey(md5value) Then hashList.Add(md5value, New SFile)) - End With - End If - - For i = _TempMediaList.Count - 1 To 0 Step -1 - data = _TempMediaList(i) - If missingMD5(data) Then - ThrowAny(Token) - data.MD5 = __getMD5(data, True) - If Not data.MD5.IsEmptyString Then - If hashList.ContainsKey(data.MD5) Then - _TempMediaList.RemoveAt(i) - Else - hashList.Add(data.MD5, New SFile) - _TempMediaList(i) = data - End If - End If - End If - Next +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim PostID$ = RegexReplace(Data.URL, RParams.DM("(?<=/)\d+", 0)) + If Not PostID.IsEmptyString Then + Dim r$ = Responser.GetResponse(String.Format(SinglePostUrl, PostID),, EDP.ReturnValue) + If Not r.IsEmptyString Then + Using j As EContainer = JsonDocument.Parse(r) + If j.ListExists Then ObtainMedia(j, j.Value("id"), j.Value("created_at")) + End Using End If - Catch ex As Exception - ProcessException(ex, Token, "ValidateMD5",, VALIDATE_MD5_ERROR) - End Try + End If End Sub #End Region -#Region "Get video static" - Friend Shared Function GetVideoInfo(ByVal URL As String, ByVal resp As Responser) As IEnumerable(Of UserMedia) - Try - If URL.Contains("twitter") Then - Dim PostID$ = RegexReplace(URL, RParams.DM("(?<=/)\d+", 0)) - If Not PostID.IsEmptyString Then - Dim r$ - Using rc As Responser = resp.Copy() : r = rc.GetResponse(String.Format(SinglePostUrl, PostID),, EDP.ReturnValue) : End Using - If Not r.IsEmptyString Then - Using j As EContainer = JsonDocument.Parse(r) - If j.ListExists Then - Dim u$ = GetVideoNodeURL(j) - If Not u.IsEmptyString Then Return {MediaFromData(u, PostID, String.Empty,,, UTypes.Video)} - End If - End Using - End If - End If - End If - Return Nothing - Catch ex As Exception - Return ErrorsDescriber.Execute(EDP.ShowMainMsg + EDP.SendInLog, ex, $"Twitter standalone downloader: fetch media error ({URL})") - End Try - End Function -#End Region #Region "Picture options" Private Function GetPictureOption(ByVal w As EContainer) As String Const P4K As String = "4096x4096" @@ -541,10 +426,10 @@ Namespace API.Twitter End Function #End Region #Region "Create media" - Private Shared Function MediaFromData(ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, - Optional ByVal _PictureOption As String = Nothing, - Optional ByVal State As UStates = UStates.Unknown, - Optional ByVal Type As UTypes = UTypes.Undefined) As UserMedia + Private Function MediaFromData(ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, + Optional ByVal _PictureOption As String = Nothing, + Optional ByVal State As UStates = UStates.Unknown, + Optional ByVal Type As UTypes = UTypes.Undefined) As UserMedia _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) Dim m As New UserMedia(_URL) With {.PictureOption = _PictureOption, .Post = New UserPost With {.ID = PostID}, .Type = Type} If Not m.URL.IsEmptyString Then m.File = CStr(RegexReplace(m.URL, FilesPattern)) diff --git a/SCrawler/API/UserDataBind.vb b/SCrawler/API/UserDataBind.vb index adcb318..86d3b2a 100644 --- a/SCrawler/API/UserDataBind.vb +++ b/SCrawler/API/UserDataBind.vb @@ -49,13 +49,10 @@ Namespace API _CollectionName = NewName If Count > 0 Then Collections.ForEach(Sub(c) c.CollectionName = NewName) End Sub - Friend Overrides Property Name As String + Friend Overrides ReadOnly Property Name As String Get Return CollectionName End Get - Set(ByVal NewCollectionName As String) - CollectionName = NewCollectionName - End Set End Property Friend Overrides Property FriendlyName As String Get @@ -367,7 +364,7 @@ Namespace API #End Region #Region "Open site, folder" Friend Overrides Sub OpenSite(Optional ByVal e As ErrorsDescriber = Nothing) - If Not e.Exists Then e = New ErrorsDescriber(EDP.SendInLog) + If Not e.Exists Then e = New ErrorsDescriber(EDP.SendToLog) 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 And Not u.HOST.Key = PathPlugin.PluginKey @@ -575,7 +572,7 @@ Namespace API MainFrameObj.ImageHandler(Me, False) Collections.ListClearDispose Dispose(False) - If Not f.IsEmptyString Then f.Delete(SFO.Path, SFODelete.EmptyOnly + Settings.DeleteMode, EDP.SendInLog) + If Not f.IsEmptyString Then f.Delete(SFO.Path, SFODelete.EmptyOnly + Settings.DeleteMode, EDP.SendToLog) Return 2 End If Case 1 @@ -592,7 +589,7 @@ Namespace API 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) + If Not f.IsEmptyString Then f.Delete(SFO.Path, SFODelete.Default + Settings.DeleteMode, EDP.SendToLog) Downloader.UserRemove(Me) MainFrameObj.ImageHandler(Me, False) Dispose(False) diff --git a/SCrawler/API/XVIDEOS/Declarations.vb b/SCrawler/API/XVIDEOS/Declarations.vb index 4043f31..f174492 100644 --- a/SCrawler/API/XVIDEOS/Declarations.vb +++ b/SCrawler/API/XVIDEOS/Declarations.vb @@ -16,7 +16,7 @@ Namespace API.XVIDEOS 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("
    - Friend Class SiteSettings : Inherits SiteSettingsBase : Implements IDomainContainer + Friend Class SiteSettings : Inherits SiteSettingsBase #Region "Declarations" - Friend Overrides ReadOnly Property Icon As Icon Implements IDomainContainer.Icon + Friend Overrides ReadOnly Property Icon As Icon Get Return My.Resources.SiteResources.XvideosIcon_48 End Get @@ -26,21 +25,10 @@ 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 + Private ReadOnly Property SiteDomains As PropertyValue + Friend ReadOnly Property Domains As DomainsContainer Friend Property DownloadUHD As PropertyValue - Private Property Initialized As Boolean = False Implements IDomainContainer.Initialized 0 Then - If Domains.Exists(Function(d) URL.Contains(d)) Then Return New ExchangeOptions With {.UserName = URL, .Exists = True} - End If - Return Nothing - End Function - Friend Overrides Function GetSpecialData(ByVal URL As String, ByVal Path As String, ByVal AskForPath As Boolean) As IEnumerable - If Not URL.IsEmptyString And Settings.UseM3U8 Then - Dim spf$ = String.Empty - Dim f As SFile = GetSpecialDataFile(Path, AskForPath, spf) - f.Name = "video" - f.Extension = "mp4" - Using resp As Responser = Responser.Copy - Using user As New UserData With {.HOST = Settings(XvideosSiteKey)} - DirectCast(user, UserDataBase).User.File = f - Dim p As UserMedia = user.Download(URL, resp, DownloadUHD.Value, String.Empty) - If p.State = UserMedia.States.Downloaded Then p.SpecialFolder = spf : Return {p} - End Using - End Using + If Domains.Domains.Exists(Function(d) URL.Contains(d)) Then Return New ExchangeOptions(Site, URL) End If Return Nothing End Function diff --git a/SCrawler/API/XVIDEOS/UserData.vb b/SCrawler/API/XVIDEOS/UserData.vb index 931cacf..05b04c7 100644 --- a/SCrawler/API/XVIDEOS/UserData.vb +++ b/SCrawler/API/XVIDEOS/UserData.vb @@ -8,11 +8,11 @@ ' but WITHOUT ANY WARRANTY Imports System.Threading Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects Imports PersonalUtilities.Functions.XML Imports PersonalUtilities.Functions.RegularExpressions Imports PersonalUtilities.Tools.Web.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 @@ -24,8 +24,8 @@ Namespace API.XVIDEOS 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) + If Not URL.IsEmptyString Then URL = $"https://www.xvideos.com/{HtmlConverter(URL).StringTrimStart("/")}" + Title = TitleHtmlConverter(ParamsArray(2)) End If Return Me End Function @@ -43,6 +43,7 @@ Namespace API.XVIDEOS Friend Sub New() SeparateVideoFolder = False UseInternalM3U8Function = True + UseClientTokens = True End Sub Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) If Not Settings.UseM3U8 Then MyMainLOG = $"{ToStringForLog()}: File [ffmpeg.exe] not found" : Exit Sub @@ -55,6 +56,7 @@ Namespace API.XVIDEOS End Sub Private Sub DownloadUserVideo(ByVal Token As CancellationToken) Dim URL$ = String.Empty + Dim isQuickies As Boolean = False Try Dim NextPage%, d% Dim limit% = If(DownloadTopCount, -1) @@ -77,39 +79,43 @@ Namespace API.XVIDEOS URL = $"https://www.xvideos.com/{user}/videos/new/{If(NextPage = 0, String.Empty, NextPage)}" Else 'Quickies URL = $"https://www.xvideos.com/quickies-api/profilevideos/all/none/N/{ID}/{NextPage}" + isQuickies = True End If + If Not j Is Nothing Then j.Dispose() r = Responser.GetResponse(URL,, EDP.ReturnValue) If Not r.IsEmptyString Then If Not EnvirSet Then UserExists = True : UserSuspended = False : EnvirSet = True - 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 = jj.Value("id"), - .URL = $"https://www.xvideos.com/{jj.Value(n).StringTrimStart("/")}" - } - If Not p.Post.ID.IsEmptyString And Not jj.Value(n).IsEmptyString Then - If Not _TempPostsList.Contains(p.Post.ID) Then - _TempPostsList.Add(p.Post.ID) - _TempMediaList.Add(p) - d += 1 - If limit > 0 And d = limit Then Exit Do - Else - Exit Do + j = JsonDocument.Parse(r) + If Not j Is Nothing Then + 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 = jj.Value("id"), + .URL = $"https://www.xvideos.com/{jj.Value(n).StringTrimStart("/")}" + } + If Not p.Post.ID.IsEmptyString And Not jj.Value(n).IsEmptyString Then + If Not _TempPostsList.Contains(p.Post.ID) Then + _TempPostsList.Add(p.Post.ID) + _TempMediaList.Add(p) + d += 1 + If limit > 0 And d = limit Then Exit Do + Else + Exit Do + End If End If - End If - Next - Continue Do - End If - End With - End If - End With + Next + Continue Do + End If + End With + End If + .Dispose() + End With + End If End If - If Not j Is Nothing Then j.Dispose() Exit Do Loop While NextPage < 100 Next @@ -119,18 +125,12 @@ Namespace API.XVIDEOS If _TempMediaList.Count > 0 Then For i% = 0 To _TempMediaList.Count - 1 ThrowAny(Token) - _TempMediaList(i) = GetVideoData(_TempMediaList(i), Responser, MySettings.DownloadUHD.Value) + _TempMediaList(i) = GetVideoData(_TempMediaList(i)) Next _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) End If - Catch oex As OperationCanceledException - Catch dex As ObjectDisposedException Catch ex As Exception - If Responser.StatusCode = Net.HttpStatusCode.NotFound Then - UserExists = False - Else - ProcessException(ex, Token, $"data downloading error [{URL}]") - End If + ProcessException(ex, Token, $"data downloading error [{URL}]",, isQuickies) Finally If _TempMediaList.ListExists Then _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) End Try @@ -152,8 +152,16 @@ Namespace API.XVIDEOS 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) + If Responser.StatusCode = Net.HttpStatusCode.NotFound Then + If NextPage = 0 Then + MyMainLOG = $"XVIDEOS saved video playlist {URL} not found." + Exit Sub + Else + Exit Do + End If + Else + Throw New Exception(Responser.ErrorText, Responser.ErrorException) + End If End If NextPage += 1 If Not r.IsEmptyString Then @@ -174,7 +182,7 @@ Namespace API.XVIDEOS If _TempMediaList.Count > 0 Then For i% = 0 To _TempMediaList.Count - 1 ThrowAny(Token) - _TempMediaList(i) = GetVideoData(_TempMediaList(i), Responser, MySettings.DownloadUHD.Value) + _TempMediaList(i) = GetVideoData(_TempMediaList(i)) Next _TempMediaList.RemoveAll(Function(m) m.URL.IsEmptyString) End If @@ -182,19 +190,19 @@ Namespace API.XVIDEOS ProcessException(ex, Token, $"data downloading error [{URL}]") End Try End Sub - Private Function GetVideoData(ByVal Media As UserMedia, ByVal resp As Responser, ByVal DownloadUHD As Boolean) As UserMedia + Private Function GetVideoData(ByVal Media As UserMedia) As UserMedia Try If Not Media.URL.IsEmptyString Then - Dim r$ = resp.GetResponse(Media.URL) + Dim r$ = Responser.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) + r = Responser.GetResponse(NewUrl) If Not r.IsEmptyString Then 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 And Not MySettings.DownloadUHD.Value Then ls.RemoveAll(Function(v) Not v.Value.ValueBetween(1, 1080)) If ls.ListExists Then ls.Sort() NewUrl = $"{appender}/{ls(0).Data.StringTrimStart("/")}" @@ -228,31 +236,28 @@ Namespace API.XVIDEOS Return Nothing End Try End Function - Friend Function Download(ByVal URL As String, ByVal resp As Responser, ByVal DownloadUHD As Boolean, ByVal ID As String) - 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, f) - m.File = f - m.State = UStates.Downloaded - Catch ex As Exception - m.State = UStates.Missing - End Try - End If - Return m - End Function 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(Media.URL, Media.PictureOption, DestinationFile) + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim m As UserMedia = GetVideoData(New UserMedia(Data.URL, UTypes.VideoPre)) + If Not m.URL.IsEmptyString Then _TempMediaList.Add(m) + End Sub + Protected Overrides Function DownloadM3U8(ByVal URL As String, ByVal Media As UserMedia, ByVal DestinationFile As SFile, ByVal Token As CancellationToken) As SFile + Return M3U8.Download(Media.URL, Media.PictureOption, DestinationFile, Token, If(UseInternalM3U8Function_UseProgress, Progress, Nothing)) 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 - Return 0 + Dim isQuickies As Boolean = False + If Not IsNothing(EObj) AndAlso TypeOf EObj Is Boolean Then isQuickies = CBool(EObj) + If Responser.StatusCode = Net.HttpStatusCode.NotFound Then + UserExists = False + Return 1 + ElseIf isQuickies And Responser.StatusCode = Net.HttpStatusCode.InternalServerError Then + Return 1 + Else + Return 0 + End If End Function End Class End Namespace \ No newline at end of file diff --git a/SCrawler/API/Xhamster/Declarations.vb b/SCrawler/API/Xhamster/Declarations.vb index 0d07a8c..22ae673 100644 --- a/SCrawler/API/Xhamster/Declarations.vb +++ b/SCrawler/API/Xhamster/Declarations.vb @@ -13,7 +13,6 @@ Namespace API.Xhamster Friend Const XhamsterSiteKey As String = "AndyProgram_XHamster" Friend ReadOnly HtmlScript As RParams = RParams.DMS("\