[
  {
    "path": ".github/ISSUE_TEMPLATE/bug反馈.md",
    "content": "---\nname: Bug反馈\nabout: 反馈Bug\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Bug描述**\n简单描述一下bug，最好附带uid，方便具体排查原因。\n\n**如何复现**\n复现步骤：\n1. \n2. \n3. \n\n**截图**\n错误信息截图\n\n**软件版本:**\n - OS: [e.g. iOS]\n - 版本 [e.g. 22]\n\n**额外描述**\n额外描述信息\n"
  },
  {
    "path": ".github/workflows/dotnet-desktop.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\n# This workflow will build, test, sign and package a WPF or Windows Forms desktop application\n# built on .NET Core.\n# To learn how to migrate your existing application to .NET Core,\n# refer to https://docs.microsoft.com/en-us/dotnet/desktop-wpf/migration/convert-project-from-net-framework\n#\n# To configure this workflow:\n#\n# 1. Configure environment variables\n# GitHub sets default environment variables for every workflow run.\n# Replace the variables relative to your project in the \"env\" section below.\n#\n# 2. Signing\n# Generate a signing certificate in the Windows Application\n# Packaging Project or add an existing signing certificate to the project.\n# Next, use PowerShell to encode the .pfx file using Base64 encoding\n# by running the following Powershell script to generate the output string:\n#\n# $pfx_cert = Get-Content '.\\SigningCertificate.pfx' -Encoding Byte\n# [System.Convert]::ToBase64String($pfx_cert) | Out-File 'SigningCertificate_Encoded.txt'\n#\n# Open the output file, SigningCertificate_Encoded.txt, and copy the\n# string inside. Then, add the string to the repo as a GitHub secret\n# and name it \"Base64_Encoded_Pfx.\"\n# For more information on how to configure your signing certificate for\n# this workflow, refer to https://github.com/microsoft/github-actions-for-desktop-apps#signing\n#\n# Finally, add the signing certificate password to the repo as a secret and name it \"Pfx_Key\".\n# See \"Build the Windows Application Packaging project\" below to see how the secret is used.\n#\n# For more information on GitHub Actions, refer to https://github.com/features/actions\n# For a complete CI/CD sample to get started with GitHub Action workflows for Desktop Applications,\n# refer to https://github.com/microsoft/github-actions-for-desktop-apps\n\nname: .NET Core Desktop\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n\n  build:\n\n    strategy:\n      matrix:\n        configuration: [Debug, Release]\n\n    runs-on: windows-latest  # For a list of available runner types, refer to\n                             # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on\n\n    env:\n      Solution_Name: WeiboAlbumDownloader.sln                         # Replace with your solution name, i.e. MyWpfApp.sln.\n      Test_Project_Path: your-test-project-path                 # Replace with the path to your test project, i.e. MyWpfApp.Tests\\MyWpfApp.Tests.csproj.\n      Wap_Project_Directory: your-wap-project-directory-name    # Replace with the Wap project directory relative to the solution, i.e. MyWpfApp.Package.\n      Wap_Project_Path: your-wap-project-path                   # Replace with the path to your Wap project, i.e. MyWpf.App.Package\\MyWpfApp.Package.wapproj.\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v3\n      with:\n        fetch-depth: 0\n\n    # Install the .NET Core workload\n    - name: Install .NET Core\n      uses: actions/setup-dotnet@v3\n      with:\n        dotnet-version: 6.0.x\n\n    # Add  MSBuild to the PATH: https://github.com/microsoft/setup-msbuild\n    - name: Setup MSBuild.exe\n      uses: microsoft/setup-msbuild@v1.0.2\n\n    # Execute all unit tests in the solution\n    - name: Execute unit tests\n      run: dotnet test\n      \n    # Upload bin directory\n    - name: Upload bin directory\n      uses: actions/upload-artifact@main\n      if: ${{ success() }}\n      with:\n        name: WeiboAlbumDownloader_Build\n        path: ./bin/Release/net6.0-windows/\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n/Demos/Demos.Algorithm/Demos.Algorithm.Toolbox.MaojiaExamine/CameraDevices.cs\n/Demos/Demos.Algorithm/Demo.Algorithm.Toolbox.Maojia/Demo.Algorithm.Toolbox.Maojia.csproj\n\n\n/WeiboAlbumDownloader/DownLoad\nSettings.json\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Vincent Wang\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# WeiboAlbumDownloader\n微博相册下载工具C#版，界面全新设计\n\n\n# 项目说明\n\n本项目可以批量采集指定微博账号下的所有图片/视频/LivePhoto。\n\n和其他语言比如python版本类似，都可以实现用户相册的采集下载工作。不过本软件UI更加美观好看。下载文件名以日期+博文+编号命名。本软件还额外增加自动修改文件日期为发博日期的功能，方便用户按照日期就行分组和排序。这个是其他工具暂时没有的功能。软件采用Selenium自动化扫码获取cookie，大大提高采集成功率。以及随机延时模拟人类刷微博的操作。\n\n![image-20231227192302467](./img/a.jpg)\n![image-20231227192302467](./img/b.jpg)\n\n\n\n# 推荐类似\n\n[银杏下载器(抖音/快手/小红书/推特作者主页作品下载) 抖音解析、快手解析、小红书解析、推特解析](https://github.com/hupo376787/GinkgoDownloader)\n\n\n# 最佳CP\n\n[银杏美女吧(基于深度学习和计算机视觉领域的尖端YOLO11模型，快速检测并筛选出需要的美女图片。) ](https://github.com/hupo376787/GinkgoBeautySelector)\n\n\n\n\n# 软件优势\n\n开源的不开源的微博下载工具很多，但是大部分都有一个致命的缺点，就是下载的图片，并没有修改日期为发博时间。就是当我想按照日期筛选排序的时候，就很尴尬了。他们的日期一般都是默认图片下载日期，这会给使用者造成很大的困扰。\n\n本软件基于这一点，在下载完图片后，自动获取发博时间（年月日时分秒），然后**将图片的创建日期、修改日期、访问日期都修改为发博时间**。虽然就是一个很简单的功能，但是为后期图片分析节约了大量的时间。\n\n\n\n# 使用说明\n\n获取微博用户uid以及web版微博Cookie，~~填入到软件根目录的Settings.json中即可~~，点击设置里面扫码获取即可。\n\n如果想要在下载完发送push+通知，请填写PushPlusToken字段。不填就不发送。\n\n如果想要开启定时任务，即在某个时间自动触发批量下载任务，那么需要填写EnableCrontab和Crontab字段。\n\n| 字段           | 说明                                                         |\n| -------------- | ------------------------------------------------------------ |\n| WeiboCnCookie  | [weibo.cn](https://weibo.cn/) Cookie                         |\n| WeiboComCookie | [weibo.com](https://weibo.com/) Cookie                       |\n| PushPlusToken  | 推送到微信，填了就会发送                                     |\n| ShowHeadImage  | 首页是否显示头像                        |\n| EnableCrontab  | 否开启Crontab定时任务                                        |\n| Crontab        | Crontab定时任务，例如\"14 2 * * *\"表示每天凌晨2点14分开始执行 |\n| EnableDatetimeRange        | 是否启用时间范围 |\n| StartDateTime        | 设置截至时间，例如\"2025/1/1\"表示下载2025年1月1日至今的所有图片 |\n| CountDownloadedSkipToNextUser  | 自动跳到下一个用户的计数器，比如设置20，那么程序会检测本地如果达到20个已经存在的图片了，就认为当前用户已经下载过了，接下来就停止程序或者跳到下一个用户                        |\n\n\n\n# 数据源\n\n推荐使用m.weibo.cn。\n\n数据源区分[weibo.com](https://weibo.com/) 和[weibo.cn](https://weibo.cn/) ，[weibo.com](https://weibo.com/) 是获取用户相册的数据（不包含视频），返回的是json格式。 [weibo.cn](https://weibo.cn/) 是获取的用户的时间流数据（包含视频），返回的是html格式。\n\n对于某些用户（可能时间线很长的用户）来说，数据源选择[weibo.com](https://weibo.com/) 可能采集到之前的某一个时间点，就没有数据了。我遇到过一个用户就是这样。\n\n还有一部分视频，无法下载，用网页访问发现无权限，暂时不知道怎么下载，以后有空再研究。\n\n\n\n# 获取uid以及Cookie\n\nPC打开[weibo.com](https://weibo.com/)，点击某一用户头像，进入主页。uid就是地址栏中的最后一串数字，比如https://weibo.com/u/1000000000。\n\n\n\nCookie可以通过点击上方按钮打开页面扫码获取，或者按F12进入控制台，网络-全部，在名称栏选择uid，标头-请求标头-Cookie。右键复制后请填入到Seetings.json。\n\n\n\n[weibo.com](https://weibo.com/)和[weibo.cn](https://weibo.cn/) cookie不一样，请注意区分。\n\n\n\n# 参考\n\n本软件的实现参考/使用了一下项目/技术：\n\n1. [微软WPF](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/?view=netdesktop-8.0)，本程序的基础。\n\n2. [MicaWPF](https://github.com/Simnico99/MicaWPF)，实现窗体Mica/Acrylic效果。\n\n3. [Newtonsoft.Json](https://www.newtonsoft.com/)，解析api返回的json数据。\n\n4. [HtmlAgilityPack](https://html-agility-pack.net/)，解析网页返回的html数据。\n\n5. [Selenium](https://www.selenium.dev/)，开源的浏览器自动化工具。\n\n6. [PushPlus](https://www.pushplus.plus/)，发送消息到微信等即时工具。\n\n7. [CronExpressionDescriptor](https://github.com/bradymholt/cron-expression-descriptor)，翻译crontab数据为可阅读的文本。\n\n8. [TimeCrontab](https://github.com/MonkSoul/TimeCrontab)，解析crontab时间数据。\n\n9. [Weibo Spider](https://github.com/dataabc/weiboSpider)，一个开源微博爬虫。\n\n10. 图标来自[FlatIcon](https://www.flaticon.com/)。\n\n11. [免责声明](https://github.com/JoeanAmier/TikTokDownloader/blob/master/README.md)\n\n\n# 是否支持项目个性化定制\n\n支持，具体需求可以联系作者，或者提issue。\n\n   \n\n# 免责声明(Disclaimers)\n\n- 使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。\n- 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。\n- 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。\n- 使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。\n- 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的各种情况负全部责任。\n\n**在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险和后果。**\n"
  },
  {
    "path": "WeiboAlbumDownloader/App.xaml",
    "content": "﻿<Application\n  x:Class=\"WeiboAlbumDownloader.App\"\n  xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n  xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n  xmlns:local=\"clr-namespace:WeiboAlbumDownloader\"\n  xmlns:mica=\"clr-namespace:MicaWPF.Styles;assembly=MicaWPF\"\n  StartupUri=\"MainWindow.xaml\">\n  <Application.Resources>\n    <ResourceDictionary>\n      <ResourceDictionary.MergedDictionaries>\n        <ResourceDictionary Source=\"pack://application:,,,/MicaWPF;component/Styles/Themes/MicaDark.xaml\" />\n        <ResourceDictionary Source=\"pack://application:,,,/MicaWPF;component/Styles/MicaWPF.xaml\" />\n        <mica:ThemeDictionary Theme=\"Light\" />\n        <!--  And Here (You can change to Light or Dark here)  -->\n        <mica:ControlsDictionary />\n        <!--  This is mandatory  -->\n\n      </ResourceDictionary.MergedDictionaries>\n    </ResourceDictionary>\n  </Application.Resources>\n</Application>\n"
  },
  {
    "path": "WeiboAlbumDownloader/App.xaml.cs",
    "content": "﻿using Sentry;\nusing System;\nusing System.Reflection;\nusing System.Windows;\nusing System.Windows.Threading;\n\nnamespace WeiboAlbumDownloader\n{\n    /// <summary>\n    /// Interaction logic for App.xaml\n    /// </summary>\n    public partial class App : Application\n    {\n        public App()\n        {\n            DispatcherUnhandledException += App_DispatcherUnhandledException;\n            SentrySdk.Init(o =>\n            {\n                // Tells which project in Sentry to send events to:\n                o.Dsn = \"https://dd07c62f4012a941162c1101ab58cf06@o1189682.ingest.us.sentry.io/4508093180411904\";\n                // When configuring for the first time, to see what the SDK is doing:\n                o.Debug = true;\n                // Set TracesSampleRate to 1.0 to capture 100% of transactions for tracing.\n                // We recommend adjusting this value in production.\n                o.TracesSampleRate = 1.0;\n            });\n        }\n\n        void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)\n        {\n            SentrySdk.AddBreadcrumb(\n                message: $\"weibo.com/u/{GlobalVar.gId}，uid: {GlobalVar.gId}，DataSource: {GlobalVar.gDataSource}，Page:{GlobalVar.gPage}，SinceId: {GlobalVar.gSinceId}\",\n                category: \"error\",\n                level: BreadcrumbLevel.Error\n            );\n            SentrySdk.CaptureException(e.Exception);\n\n            // If you want to avoid the application from crashing:\n            e.Handled = true;\n        }\n\n        protected override void OnStartup(StartupEventArgs e)\n        {\n            base.OnStartup(e);\n            SentrySdk.ConfigureScope(scope =>\n            {\n                scope.SetTag(\"AppName\", Assembly.GetExecutingAssembly().GetName().Name!);\n                scope.SetTag(\"DeviceName\", Environment.MachineName);\n                scope.SetTag(\"DeviceName\", WeiboAlbumDownloader.MainWindow.currentVersion.ToString(\"#0.0\"));\n            });\n        }\n\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/AssemblyInfo.cs",
    "content": "using System.Windows;\n\n[assembly: ThemeInfo(\n    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located\n                                     //(used if a resource is not found in the page,\n                                     // or application resource dictionaries)\n    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located\n                                              //(used if a resource is not found in the page,\n                                              // app, or any theme specific resource dictionaries)\n)]\n"
  },
  {
    "path": "WeiboAlbumDownloader/Converters/ColorConverter.cs",
    "content": "﻿using System;\nusing System.Globalization;\nusing System.Windows.Data;\nusing System.Windows.Media;\nusing WeiboAlbumDownloader.Enums;\n\nnamespace WeiboAlbumDownloader.Converters\n{\n    public class ColorConverter : IValueConverter\n    {\n        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)\n        {\n            var type = (MessageEnum)value;\n            if (type == MessageEnum.Error)\n                return new SolidColorBrush(Colors.Red);\n            else if (type == MessageEnum.Warning)\n                return new SolidColorBrush(Colors.Yellow);\n            else if (type == MessageEnum.Success)\n                return new SolidColorBrush(Colors.Green);\n\n            return new SolidColorBrush(Colors.Orange);\n        }\n\n        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)\n        {\n            throw new NotImplementedException();\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Enums/MessageEnum.cs",
    "content": "﻿namespace WeiboAlbumDownloader.Enums\n{\n    public enum MessageEnum\n    {\n        Info,\n        Warning,\n        Success,\n        Error\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Enums/PicEnum.cs",
    "content": "﻿namespace WeiboAlbumDownloader.Enums\n{\n    public enum PicEnum\n    {\n        //单图\n        Picture,\n        //组图\n        Pictures,\n        //视频\n        Video\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Enums/WeiboDataSource.cs",
    "content": "﻿using System.ComponentModel;\n\nnamespace WeiboAlbumDownloader.Enums\n{\n    public enum WeiboDataSource\n    {\n        [Description(\"m.weibo.cn\")]\n        WeiboCnMobile = 0,\n        [Description(\"weibo.cn\")]\n        WeiboCn = 1,\n        [Description(\"weibo.com1\")]\n        WeiboCom1 = 2,\n        [Description(\"weibo.com2\")]\n        WeiboCom2 = 3,\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/GlobalVar.cs",
    "content": "﻿using WeiboAlbumDownloader.Enums;\n\nnamespace WeiboAlbumDownloader\n{\n    public class GlobalVar\n    {\n        public static string gId = \"\";\n        public static long gSinceId;\n        public static int gPage;\n        public static WeiboDataSource gDataSource;\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Helpers/GithubHelper.cs",
    "content": "﻿using System;\nusing System.Diagnostics;\nusing System.Net.Http;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\n\nnamespace WeiboAlbumDownloader.Helpers\n{\n    public class GithubHelper\n    {\n        public static async Task<string?> GetLatestVersion()\n        {\n            try\n            {\n                HttpClient client = new HttpClient();\n                var resp = await client.GetAsync(\"https://github.com/hupo376787/WeiboAlbumDownloader/tags\");\n                var body = await resp.Content.ReadAsStringAsync();\n                string pattern = \"tags.*zip\";\n                //匹配多个\n                //MatchCollection list = Regex.Matches(body, pattern);\n                //匹配第一个\n                Match match = Regex.Match(body, pattern);\n                if (match.Success)\n                {\n                    var version = match.Value.Replace(\"tags/\", \"\").Replace(\".zip\", \"\");\n                    return version;\n                }\n\n                return null;\n\n            }\n            catch (Exception ex)\n            {\n                return null;\n            }\n        }\n\n        public static async Task<string> GetGiteeLatestVersion()\n        {\n            try\n            {\n                HttpClient client = new HttpClient();\n                var resp = await client.GetAsync(\"https://gitee.com/hupo376787/weibo-album-downloader/tags\");\n                var body = await resp.Content.ReadAsStringAsync();\n                string pattern = @\"<a\\s+title=\"\"(\\d+\\.\\d+)\"\"\";\n                Regex regex = new Regex(pattern);\n                //MatchCollection matches = regex.Matches(body);\n                Match match = Regex.Match(body, pattern);\n                if (match.Success)\n                {\n                    var version = match.Value.Replace(\"<a title=\\\"\", \"\").Replace(\"\\\"\", \"\");\n                    return version;\n                }\n\n                return null;\n            }\n            catch (Exception ex)\n            {\n                return null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Helpers/HttpHelper.cs",
    "content": "﻿using Newtonsoft.Json;\nusing System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.IO.Compression;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading.Tasks;\nusing System.Windows;\nusing WeiboAlbumDownloader.Enums;\nusing WeiboAlbumDownloader.Models;\n\nnamespace WeiboAlbumDownloader.Helpers\n{\n    public class HttpHelper\n    {\n        static HttpClient client;\n        /// <summary>\n        /// \n        /// </summary>\n        /// <typeparam name=\"T\"></typeparam>\n        /// <param name=\"url\"></param>\n        /// <param name=\"fileName\">不为空的时候，表示存图</param>\n        /// <returns></returns>\n        public static async Task<T> GetAsync<T>(string url, WeiboDataSource dataSource, string cookie, string fileName = \"\", Action<string, MessageEnum> logAction = null)\n        {\n            string responseBody = null;\n            try\n            {\n                //logAction?.Invoke($\"[Request URL]: {url}\", MessageEnum.Info);\n                //logAction?.Invoke($\"[Request Cookie]: {cookie}\", MessageEnum.Info);\n\n                var handler = new HttpClientHandler()\n                {\n                    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate\n                };\n                client = new HttpClient(handler);\n                var request = new HttpRequestMessage(HttpMethod.Get, url);\n                request.Headers.Add(\"Referer\", \"https://m.weibo.cn/\");\n                if (string.IsNullOrEmpty(fileName))\n                {\n                    request.Headers.Add(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36\");\n                    request.Headers.Add(\"Accept\", \"application/json, text/plain, */*\");\n                    request.Headers.Add(\"Accept-Encoding\", \"gzip, deflate, br, zstd\");\n                    request.Headers.Add(\"Accept-Language\", \"zh-CN,zh;q=0.9\");\n                    request.Headers.Add(\"Cookie\", cookie);\n                }\n\n                var response = await client.SendAsync(request);\n\n                var contentEncoding = response.Content.Headers.ContentEncoding.ToString();\n\n                if (contentEncoding.Contains(\"gzip\"))\n                {\n                    using (var stream = await response.Content.ReadAsStreamAsync())\n                    using (var decompressedStream = new GZipStream(stream, CompressionMode.Decompress))\n                    using (var reader = new StreamReader(decompressedStream))\n                    {\n                        responseBody = await reader.ReadToEndAsync();\n                    }\n                }\n                else\n                {\n                    responseBody = await response.Content.ReadAsStringAsync();\n                }\n\n                logAction?.Invoke($\"[Response Status Code]: {response.StatusCode}\", MessageEnum.Info);\n                //logAction?.Invoke($\"[Response Body]: {responseBody}\", MessageEnum.Info);\n\n                response.EnsureSuccessStatusCode();\n\n                if (!string.IsNullOrEmpty(fileName))\n                {\n                    string directory = Path.GetDirectoryName(fileName);\n                    string fileNameOnly = Path.GetFileName(fileName);\n\n                    var invalidChar = Path.GetInvalidFileNameChars();\n                    var cleanedFileNameOnly = invalidChar.Aggregate(fileNameOnly, (o, r) => (o.Replace(r.ToString(), string.Empty)));\n\n                    string finalFullPath;\n                    if (cleanedFileNameOnly.Length > 200)\n                    {\n                        // Truncate the file name part, then recombine with the original directory\n                        string truncatedFileName = cleanedFileNameOnly.Substring(0, 200) + Path.GetExtension(cleanedFileNameOnly);\n                        finalFullPath = Path.Combine(directory, truncatedFileName);\n                    }\n                    else\n                    {\n                        // Recombine the original directory with the cleaned file name\n                        finalFullPath = Path.Combine(directory, cleanedFileNameOnly);\n                    }\n\n                    string uniquePath = GetUniqueFileName(finalFullPath);\n\n                    var stream = response.Content.ReadAsStream();\n                    FileStream lxFS = File.Create(uniquePath);\n                    await stream.CopyToAsync(lxFS);\n                    lxFS.Close();\n                    lxFS.Dispose();\n                    stream.Dispose();\n\n                    return default(T);\n                }\n\n                Type type = typeof(T);\n                if (type == typeof(string))\n                    return (T)Convert.ChangeType(responseBody, typeof(T));\n                else\n                    return JsonConvert.DeserializeObject<T>(responseBody);\n            }\n            catch (Exception ex)\n            {\n                logAction?.Invoke($\"[Request Failed]: URL: {url}\", MessageEnum.Error);\n                if (responseBody != null && responseBody.Contains(\"<title>登录 - 微博</title>\"))\n                {\n                    logAction?.Invoke($\"[Cookie可能失效]: {responseBody}\", MessageEnum.Error);\n                }\n                logAction?.Invoke($\"[Exception]: {ex.ToString()}\", MessageEnum.Error);\n                throw;\n            }\n        }\n\n        public static string GetUniqueFileName(string filePath)\n        {\n            // 获取文件目录和扩展名\n            string directory = Path.GetDirectoryName(filePath)!;\n            string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);\n            string extension = Path.GetExtension(filePath);\n\n            int count = 1;\n\n            // 初始文件路径\n            string uniqueFilePath = filePath;\n\n            // 检查文件是否存在，如果存在则循环添加编号直到找到一个不存在的文件名\n            while (File.Exists(uniqueFilePath))\n            {\n                string newFileName = $\"{fileNameWithoutExtension}({count}){extension}\";\n                uniqueFilePath = Path.Combine(directory, newFileName);\n                count++;\n            }\n\n            return uniqueFilePath;\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Helpers/PushPlusHelper.cs",
    "content": "﻿using System.Diagnostics;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nnamespace WeiboAlbumDownloader.Helpers\n{\n    public class PushPlusHelper\n    {\n        private static readonly HttpClient client = new HttpClient();\n        public static async Task SendMessage(string token, string title, string content)\n        {\n            if (string.IsNullOrEmpty(token))\n            {\n                return;\n            }\n\n            using HttpResponseMessage response = await client.GetAsync($\"http://www.pushplus.plus/send?token={token}&title={title}&content={content}\");\n            response.EnsureSuccessStatusCode();\n\n            var jsonResponse = await response.Content.ReadAsStringAsync();\n            Debug.WriteLine($\"{jsonResponse}\");\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Helpers/SeleniumHelper.cs",
    "content": "﻿using OpenQA.Selenium;\nusing OpenQA.Selenium.Chrome;\nusing OpenQA.Selenium.Support.UI;\nusing System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing WeiboAlbumDownloader.Enums;\n\nnamespace WeiboAlbumDownloader.Helpers\n{\n    internal class SeleniumHelper\n    {\n        public static string? GetCookie(WeiboDataSource dataSource)\n        {\n            string loginUrl = \"https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https://weibo.cn\";\n            if (dataSource == WeiboDataSource.WeiboCn)\n                loginUrl = \"https://passport.weibo.com/sso/signin?entry=wapsso&source=wapssowb&url=https://weibo.cn\";\n            else if (dataSource == WeiboDataSource.WeiboCnMobile)\n                loginUrl = \"https://passport.weibo.com/sso/signin?entry=wapsso&source=wapsso&url=https://m.weibo.cn\";\n            else\n                loginUrl = \"https://passport.weibo.com/sso/signin?entry=miniblog&source=miniblog&url=https://weibo.com/\";\n\n            IWebDriver driver = new ChromeDriver();\n            driver.Url = loginUrl;\n            WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromHours(8))\n            {\n                PollingInterval = TimeSpan.FromMilliseconds(500),\n            };\n            wait.IgnoreExceptionTypes(typeof(NoSuchElementException));\n            //wait.Until(d => d.FindElement(By.LinkText(\"title\")));\n\n            // 等待页面加载完成并获取页面标题\n            wait.Until(d => d.Title.Equals(\"微博 – 随时随地发现新鲜事\") || d.Title.Equals(\"我的首页\") || d.Title.Equals(\"微博\"));\n\n            // 获取页面标题并进行检查\n            string pageTitle = driver.Title;\n            if (pageTitle.Equals(\"微博 – 随时随地发现新鲜事\") || pageTitle.Equals(\"我的首页\") || pageTitle.Equals(\"微博\"))\n            {\n                //AppendLog(\"扫码登陆成功\", MessageEnum.Success);\n                // 获取所有的 Cookie 对象\n                IReadOnlyCollection<Cookie> cookies = driver.Manage().Cookies.AllCookies;\n\n                // 将 Cookie 对象转换为一个字符串，格式类似于 HTTP 请求头的 Cookie 字符串\n                string cookie = string.Join(\"; \", cookies.Select(c => $\"{c.Name}={c.Value}\"));\n\n                // 打印 Cookie 字符串\n                Debug.WriteLine(cookie);\n\n                driver.Quit();\n\n                return cookie;\n            }\n            else\n            {\n                Debug.WriteLine(\"未登录\");\n            }\n\n            // 程序结束时，手动关闭浏览器\n            driver.Quit();\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Helpers/WeiboMidHelper.cs",
    "content": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Net.Http;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Newtonsoft.Json.Linq;\n\nnamespace WeiboAlbumDownloader.Helpers\n{\n    public static class WeiboMidHelper\n    {\n        private static readonly HttpClient Http = CreateClient();\n\n        public static async Task<List<string>> GetImageIdsByMidAsync(string mid, string? cookie = null, CancellationToken ct = default)\n        {\n            if (string.IsNullOrWhiteSpace(mid))\n                return new List<string>();\n\n            // 1) PC ӿ\n            var pc = await TryFromPcAsync(mid, cookie, ct);\n            if (pc.Count > 0) return pc;\n\n            // 2) ƶӿ\n            var mobile = await TryFromMobileAsync(mid, cookie, ct);\n            return mobile;\n        }\n\n        private static async Task<List<string>> TryFromPcAsync(string mid, string? cookie, CancellationToken ct)\n        {\n            var url = $\"https://weibo.com/ajax/statuses/show?id={mid}&locale=zh-CN&isGetLongText=true\";\n            using var req = new HttpRequestMessage(HttpMethod.Get, url);\n            req.Headers.Referrer = new Uri(\"https://weibo.com/\");\n            req.Headers.TryAddWithoutValidation(\"X-Requested-With\", \"XMLHttpRequest\");\n            if (!string.IsNullOrWhiteSpace(cookie))\n            {\n                req.Headers.TryAddWithoutValidation(\"Cookie\", cookie);\n                var xsrf = TryGetCookieValue(cookie, \"XSRF-TOKEN\");\n                if (!string.IsNullOrEmpty(xsrf))\n                    req.Headers.TryAddWithoutValidation(\"X-XSRF-TOKEN\", xsrf);\n            }\n\n            using var resp = await Http.SendAsync(req, ct);\n            if (!resp.IsSuccessStatusCode) return new List<string>();\n\n            var json = await resp.Content.ReadAsStringAsync(ct);\n            return ExtractPicIds(json);\n        }\n\n        private static async Task<List<string>> TryFromMobileAsync(string mid, string? cookie, CancellationToken ct)\n        {\n            var url = $\"https://m.weibo.cn/statuses/show?id={mid}\";\n            using var req = new HttpRequestMessage(HttpMethod.Get, url);\n            req.Headers.Referrer = new Uri($\"https://m.weibo.cn/detail/{mid}\");\n            req.Headers.TryAddWithoutValidation(\"X-Requested-With\", \"XMLHttpRequest\");\n            if (!string.IsNullOrWhiteSpace(cookie))\n                req.Headers.TryAddWithoutValidation(\"Cookie\", cookie);\n\n            using var resp = await Http.SendAsync(req, ct);\n            if (!resp.IsSuccessStatusCode) return new List<string>();\n\n            var json = await resp.Content.ReadAsStringAsync(ct);\n            return ExtractPicIds(json);\n        }\n\n        private static List<string> ExtractPicIds(string json)\n        {\n            var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);\n            if (string.IsNullOrWhiteSpace(json)) return result.ToList();\n\n            var root = JToken.Parse(json);\n\n            //  m.weibo.cn: { ok: 1, data: {...} }\n            if (root[\"ok\"] != null && root[\"data\"] != null)\n                root = root[\"data\"]!;\n\n            void CollectFromNode(JToken? node)\n            {\n                if (node == null) return;\n\n                //  pic_ids\n                var picIds = node[\"pic_ids\"] as JArray;\n                if (picIds != null)\n                {\n                    foreach (var id in picIds.Values<string>())\n                        if (!string.IsNullOrWhiteSpace(id)) result.Add(id);\n                }\n\n                //  pics[].pid\n                var pics = node[\"pics\"] as JArray;\n                if (pics != null)\n                {\n                    foreach (var p in pics)\n                    {\n                        var pid = p?[\"pid\"]?.Value<string>();\n                        if (!string.IsNullOrWhiteSpace(pid)) result.Add(pid!);\n                    }\n                }\n\n                // ת\n                if (node[\"retweeted_status\"] != null)\n                    CollectFromNode(node[\"retweeted_status\"]);\n            }\n\n            CollectFromNode(root);\n\n            return result.ToList();\n        }\n\n        private static string? TryGetCookieValue(string cookieHeader, string key)\n        {\n            //  Cookie ıȡĳֵ\n            // : \"SUB=...; XSRF-TOKEN=abc; SUBP=...;\"\n            if (string.IsNullOrWhiteSpace(cookieHeader) || string.IsNullOrWhiteSpace(key))\n                return null;\n\n            var parts = cookieHeader.Split(';');\n            foreach (var p in parts)\n            {\n                var kv = p.Split('=', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);\n                if (kv.Length == 2 && kv[0].Equals(key, StringComparison.OrdinalIgnoreCase))\n                    return kv[1];\n            }\n            return null;\n        }\n\n        private static HttpClient CreateClient()\n        {\n            var handler = new HttpClientHandler\n            {\n                AllowAutoRedirect = true,\n                AutomaticDecompression = DecompressionMethods.All\n            };\n\n            var c = new HttpClient(handler)\n            {\n                Timeout = TimeSpan.FromSeconds(15)\n            };\n\n            c.DefaultRequestHeaders.UserAgent.ParseAdd(\n                \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\");\n            c.DefaultRequestHeaders.Accept.ParseAdd(\"application/json, text/plain, */*\");\n            c.DefaultRequestHeaders.AcceptLanguage.ParseAdd(\"zh-CN,zh;q=0.9,en;q=0.8\");\n\n            return c;\n        }\n    }\n}"
  },
  {
    "path": "WeiboAlbumDownloader/MainWindow.xaml",
    "content": "﻿<mica:MicaWindow\n  x:Class=\"WeiboAlbumDownloader.MainWindow\"\n  xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n  xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n  xmlns:conv=\"clr-namespace:WeiboAlbumDownloader.Converters\"\n  xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n  xmlns:local=\"clr-namespace:WeiboAlbumDownloader\"\n  xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n  xmlns:mica=\"clr-namespace:MicaWPF.Controls;assembly=MicaWPF\"\n  Title=\"微博相册/视频/LivePhoto下载\"\n  Width=\"1200\"\n  Height=\"800\"\n  FontFamily=\"微软雅黑\"\n  Icon=\"/weibo.ico\"\n  SizeChanged=\"MicaWindow_SizeChanged\"\n  SystemBackdropType=\"Acrylic\"\n  TitleBarType=\"WinUI\"\n  WindowStartupLocation=\"CenterScreen\"\n  mc:Ignorable=\"d\">\n  <Window.Resources>\n    <conv:ColorConverter x:Key=\"colorConverter\" />\n  </Window.Resources>\n\n  <Grid>\n    <Grid.RowDefinitions>\n      <RowDefinition Height=\"Auto\" />\n      <RowDefinition Height=\"Auto\" />\n      <RowDefinition />\n    </Grid.RowDefinitions>\n\n    <Grid Margin=\"12\">\n      <Grid.ColumnDefinitions>\n        <ColumnDefinition Width=\"*\" />\n        <ColumnDefinition Width=\"Auto\" />\n        <ColumnDefinition Width=\"Auto\" />\n        <ColumnDefinition Width=\"Auto\" />\n        <ColumnDefinition Width=\"Auto\" />\n      </Grid.ColumnDefinitions>\n      <mica:TextBox\n        x:Name=\"TextBox_WeiboId\"\n        Grid.Column=\"0\"\n        Height=\"32\"\n        KeyUp=\"TextBox_WeiboId_KeyUp\"\n        ToolTip=\"填写uid进行单用户下载或者uidList.txt进行批量下载\"\n        Watermark=\"微博uid\" />\n      <mica:TextBox\n        x:Name=\"TextBox_StartPage\"\n        Grid.Column=\"1\"\n        Height=\"32\"\n        Margin=\"8,0,0,0\"\n        Watermark=\"起始页码(可选)\" />\n      <mica:TextBox\n        x:Name=\"TextBox_StartSinceId\"\n        Grid.Column=\"2\"\n        Height=\"32\"\n        Margin=\"8,0,0,0\"\n        Watermark=\"起始SinceID(可选)\" />\n      <StackPanel Grid.Column=\"3\" Orientation=\"Horizontal\">\n        <mica:Button\n          Width=\"110\"\n          Height=\"32\"\n          Margin=\"8,0,0,0\"\n          Click=\"StartDownLoad\"\n          CornerRadius=\"2 0 0 2\"\n          ToolTip=\"开始/停止下载，停止下载的时候可能有延后，因为需要等待分页任务下载完才能停\">\n          <StackPanel Orientation=\"Horizontal\">\n            <Image Width=\"18\" Source=\"/Assets/cloud-computing.png\" />\n            <TextBlock\n              x:Name=\"tbDownload\"\n              Margin=\"8,0,0,0\"\n              VerticalAlignment=\"Center\"\n              Text=\"开始下载\" />\n          </StackPanel>\n        </mica:Button>\n        <mica:Button\n          Grid.Column=\"1\"\n          Width=\"30\"\n          Height=\"32\"\n          Margin=\"-2,0,0,0\"\n          Click=\"BatchDownLoad\"\n          CornerRadius=\"0 2 2 0\"\n          FontSize=\"10\"\n          ToolTip=\"自动循环uidList.txt中所有用户，执行批量下载任务\">\n          <StackPanel Orientation=\"Horizontal\">\n            <Image Width=\"18\" Source=\"/Assets/batch.png\" />\n            <TextBlock\n              Margin=\"8,0,0,0\"\n              VerticalAlignment=\"Center\"\n              Text=\"批量&#x0a;下载\"\n              Visibility=\"Collapsed\" />\n          </StackPanel>\n        </mica:Button>\n      </StackPanel>\n      <mica:Button\n        Grid.Column=\"4\"\n        Width=\"64\"\n        Height=\"32\"\n        Margin=\"8,0\"\n        Click=\"OpenSettings\"\n        ToolTip=\"设置\">\n        <StackPanel Orientation=\"Horizontal\">\n          <Image Width=\"18\" Source=\"/Assets/settings.png\" />\n        </StackPanel>\n      </mica:Button>\n    </Grid>\n\n    <Grid Grid.Row=\"2\">\n      <Grid.ColumnDefinitions>\n        <ColumnDefinition x:Name=\"Column_LeftGrid\" Width=\"*\" />\n        <ColumnDefinition Width=\"2*\" />\n      </Grid.ColumnDefinitions>\n      <StackPanel VerticalAlignment=\"Center\">\n        <Border x:Name=\"Border_Head\" Margin=\"0\">\n          <Border.Background>\n            <ImageBrush\n              x:Name=\"Image_Head\"\n              ImageSource=\"/weibo.ico\"\n              Stretch=\"Fill\" />\n          </Border.Background>\n        </Border>\n        <TextBox\n          x:Name=\"TextBlock_UID\"\n          Width=\"256\"\n          Margin=\"16,16,16,0\"\n          HorizontalAlignment=\"Center\"\n          HorizontalContentAlignment=\"Center\"\n          d:Text=\"uid\"\n          Background=\"Transparent\"\n          BorderThickness=\"0\"\n          FontSize=\"22\"\n          FontWeight=\"Bold\"\n          Foreground=\"Gray\" />\n        <TextBox\n          x:Name=\"TextBlock_NickName\"\n          Width=\"256\"\n          Margin=\"16\"\n          HorizontalAlignment=\"Center\"\n          HorizontalContentAlignment=\"Center\"\n          d:Text=\"昵称\"\n          Background=\"Transparent\"\n          BorderThickness=\"0\"\n          FontSize=\"32\"\n          FontWeight=\"Bold\"\n          Foreground=\"Gray\" />\n        <TextBlock\n          x:Name=\"TextBlock_WeiboDesc\"\n          Margin=\"8,0\"\n          HorizontalAlignment=\"Center\"\n          d:Text=\"微博[888] 关注[274] 粉丝[7.9万] 分组[1]\"\n          FontSize=\"16\"\n          Foreground=\"Gray\"\n          TextWrapping=\"Wrap\" />\n      </StackPanel>\n      <ListView\n        x:Name=\"ListView_Messages\"\n        Grid.Column=\"1\"\n        Margin=\"6\"\n        Background=\"Transparent\"\n        ScrollViewer.HorizontalScrollBarVisibility=\"Disabled\">\n        <ListView.ContextMenu>\n          <ContextMenu>\n            <MenuItem Click=\"ListView_CopyLog\" Header=\"复制\" />\n            <MenuItem Click=\"ListView_ClearLog\" Header=\"清空\" />\n            <MenuItem Click=\"ListView_ExportLog\" Header=\"导出\" />\n            <MenuItem Click=\"ListView_OpenDownloadFolder\" Header=\"打开下载文件目录\" />\n          </ContextMenu>\n        </ListView.ContextMenu>\n        <ListView.ItemTemplate>\n          <DataTemplate>\n            <Grid>\n              <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"20\" />\n                <ColumnDefinition Width=\"50\" />\n                <ColumnDefinition Width=\"60\" />\n                <ColumnDefinition Width=\"*\" />\n              </Grid.ColumnDefinitions>\n              <Ellipse\n                Width=\"14\"\n                Height=\"14\"\n                VerticalAlignment=\"Top\"\n                Fill=\"{Binding Path=MessageType, Converter={StaticResource colorConverter}}\" />\n              <TextBlock Grid.Column=\"1\" Text=\"{Binding MessageType, Mode=OneWay}\" />\n              <TextBlock Grid.Column=\"2\" Text=\"{Binding Time, Mode=OneWay}\" />\n              <TextBlock\n                Grid.Column=\"3\"\n                Text=\"{Binding Message, Mode=OneWay}\"\n                TextWrapping=\"Wrap\" />\n            </Grid>\n          </DataTemplate>\n        </ListView.ItemTemplate>\n      </ListView>\n    </Grid>\n  </Grid>\n</mica:MicaWindow>\n"
  },
  {
    "path": "WeiboAlbumDownloader/MainWindow.xaml.cs",
    "content": "﻿using HtmlAgilityPack;\nusing MicaWPF.Controls;\nusing Newtonsoft.Json;\nusing Sentry;\nusing System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing System.Windows;\nusing System.Windows.Media.Imaging;\nusing TimeCrontab;\nusing WeiboAlbumDownloader.Enums;\nusing WeiboAlbumDownloader.Helpers;\nusing WeiboAlbumDownloader.Models;\n\nnamespace WeiboAlbumDownloader\n{\n    /// <summary>\n    /// Interaction logic for MainWindow.xaml\n    /// </summary>\n    public partial class MainWindow : MicaWindow\n    {\n        //发行说明：\n        //①此处升级一下GlobalVar版本号\n        //②Github/Gitee release新建一个新版本Tag\n        //③上传压缩包删除Settings.json以及uidList.txt\n        public static double currentVersion = 7.3;\n\n        /// <summary>\n        /// com1是根据uid获取相册id，https://photo.weibo.com/albums/get_all?uid=10000000000&page=1；根据uid和相册id以及相册type获取图片列表，https://photo.weibo.com/photos/get_all?uid=10000000000&album_id=3959362334782071&page=1&type=3\n        /// com2是根据uid获取相册id，https://weibo.com/ajax/profile/getImageWall?uid=10000000000&sinceid=0&has_album=true；根据相册id和sinceid获取图片列表，https://weibo.com/ajax/profile/getAlbumDetail?containerid=123456789000123456_-_pc_profile_album_-_photo_-_camera_-_0_-_%25E5%258E%259F%25E5%2588%259B&since_id=0\n        /// cn是从 https://weibo.cn/10000000000/profile?page=2获取html解析发布的微博\n        /// m.cn是移动端api，可以从 https://m.weibo.cn/api/container/getIndex?type=uid&value=10000000000&containerid=10760310000000000&since_id=5040866311798795，since_id第一次为空，containerid以107603开头是获取时间线，100505开头是个人资料\n        /// </summary>\n        private WeiboDataSource dataSource = WeiboDataSource.WeiboCnMobile;\n        private int countPerPage = 90;\n        private string downloadFolder = Environment.CurrentDirectory + \"//DownLoad//\";\n        private SettingsModel? settings;\n        private string? cookie;\n        private List<string> uids = new List<string>();\n        //用来跳过到下一个uid的计数。如果当前uid下载的时候已存在文件超过此计数，则判定下载过了。\n        private int countDownloadedSkipToNextUser;\n        private bool isDownloading;\n        private CancellationTokenSource? cancellationTokenSource;\n        public ObservableCollection<MessageModel> Messages { get; set; } = new ObservableCollection<MessageModel>();\n\n        public MainWindow()\n        {\n            InitializeComponent();\n\n            _ = GetVersion();\n            InitUidsData();\n            InitSettingsData();\n\n            Directory.CreateDirectory(downloadFolder);\n            ListView_Messages.ItemsSource = Messages;\n\n            //定时任务\n            if (settings?.Crontab != null)\n            {\n                var cron = Crontab.Parse(settings?.Crontab);\n                Task.Factory.StartNew(async () =>\n                {\n                    while (true)\n                    {\n                        await Task.Delay((int)cron.GetSleepMilliseconds(DateTime.Now));\n\n                        if (settings?.EnableCrontab != true)\n                        {\n                            continue;\n                        }\n\n                        AppendLog(DateTime.Now.ToString(\"yyyy-MM-dd HH-mm-ss\") + \"开启定时任务\");\n\n                        foreach (var item in uids)\n                        {\n                            await Start(item.Trim());\n                            AppendLog(\"等待60s，下载下一个用户相册数据\");\n                            await Task.Delay(60 * 1000);\n                        }\n\n                        AppendLog(DateTime.Now.ToString(\"yyyy-MM-dd HH-mm-ss\") + \"结束定时任务\");\n                    }\n                }, TaskCreationOptions.LongRunning);\n            }\n        }\n\n        private async Task GetVersion()\n        {\n            //AppendLog($\"当前程序版本 V{GlobalVar.currentVersion}\");\n            var latestVersionString = await GithubHelper.GetLatestVersion();\n            if (string.IsNullOrEmpty(latestVersionString) || latestVersionString?.Length > 5)\n            {\n                latestVersionString = await GithubHelper.GetGiteeLatestVersion();\n            }\n            AppendLog($\"当前程序版本 V{currentVersion}，最新版为 V{latestVersionString}\");\n\n            var res = double.TryParse(latestVersionString, out double latestVersion);\n            if (res)\n            {\n                if (latestVersion > currentVersion)\n                {\n                    await Application.Current.Dispatcher.InvokeAsync(async () =>\n                    {\n                        var uiMessageBox = new MicaWPF.Dialogs.ContentDialog\n                        {\n                            Title = \"提示\",\n                            Content = $\"检测到新版本 V{latestVersionString}，当前程序版本 V{currentVersion}，点击确定下载\",\n                            PrimaryButtonText = \"OK\"\n                        };\n\n                        var res = await uiMessageBox.ShowAsync();\n                        if (res == MicaWPF.Core.Enums.ContentDialogResult.PrimaryButton)\n                        {\n                            Process.Start(new ProcessStartInfo(\"https://github.com/hupo376787/WeiboAlbumDownloader/releases\") { UseShellExecute = true });\n                        }\n                    });\n                }\n            }\n        }\n\n        private async Task Start(string userId, int startPage = 1, long startSinceId = 0)\n        {\n            try\n            {\n                cancellationTokenSource?.Cancel();\n                cancellationTokenSource = new CancellationTokenSource();\n\n                var conv = long.TryParse(userId, out long temp);\n                if (string.IsNullOrEmpty(userId) || !conv)\n                {\n                    AppendLog(\"错误的微博uid，参考上面说明获取uid\", MessageEnum.Error);\n                    return;\n                }\n\n                //用于Sentry崩溃收集\n                GlobalVar.gId = userId;\n                GlobalVar.gDataSource = dataSource = settings.DataSource;\n\n                //读取用户列表和设置\n                InitSettingsData();\n\n                if (startPage <= 1) startPage = 1;\n                if (startSinceId <= 0) startSinceId = 0;\n\n                await Task.Run(async () =>\n                {\n                    bool isSkip = false;\n\n                    //四种api下载\n                    {\n                        string nickName = string.Empty;\n                        string headUrl = string.Empty;\n                        int page = startPage;\n                        long sinceId = startSinceId;\n                        countDownloadedSkipToNextUser = 0;\n                        isSkip = false;\n                        AppendLog(\"开始下载\" + userId, MessageEnum.Info);\n\n                        //源是weibo.com的时候，获取的是获取相册列表，json格式\n                        if (dataSource == WeiboDataSource.WeiboCom1)\n                        {\n                            string albumsUrl = $\"https://photo.weibo.com/albums/get_all?uid={userId}&page=1\";\n                            TextBlock_UID?.Dispatcher.InvokeAsync(() =>\n                            {\n                                TextBlock_UID.Text = userId;\n                            });\n\n                            var albums = await HttpHelper.GetAsync<UserAlbumModel>(albumsUrl, dataSource, cookie!);\n                            Directory.CreateDirectory(downloadFolder + userId);\n                            if (albums != null)\n                            {\n                                foreach (var item in albums?.data?.album_list!)\n                                {\n                                    if (isSkip)\n                                        break;\n\n                                    Directory.CreateDirectory(downloadFolder + userId + \"//\" + item.caption);\n\n                                    if (item.caption == \"头像相册\")\n                                    {\n                                        headUrl = item.cover_pic;\n                                    }\n\n                                    page = startPage;\n                                    int consecutiveEmptyPages = 0;\n                                    while (true)\n                                    {\n                                        if (isSkip)\n                                            break;\n\n                                        GlobalVar.gPage = page;\n\n                                        string albumUrl = $\"https://photo.weibo.com/photos/get_all?uid={userId}&album_id={item.album_id}&count={countPerPage}&page={page}&type={item.type}\";\n                                        var photos = await HttpHelper.GetAsync<AlbumDetailModel>(albumUrl, dataSource, cookie!, logAction: AppendLog);\n                                        if (photos != null && photos.data?.photo_list != null && photos.data?.photo_list.Count > 0)\n                                        {\n                                            consecutiveEmptyPages = 0;\n                                            int photoCount = 1;\n                                            string oldCaption = \"\";\n                                            foreach (var photo in photos.data?.photo_list!)\n                                            {\n                                                if (cancellationTokenSource.IsCancellationRequested)\n                                                {\n                                                    AppendLog($\"用户手动终止，当前页码: {page}\", MessageEnum.Info);\n                                                    return;\n                                                }\n\n                                                if (oldCaption.Equals(photo.caption_render))\n                                                {\n                                                    photoCount++;\n                                                }\n                                                else\n                                                {\n                                                    photoCount = 1;\n                                                }\n\n                                                oldCaption = photo.caption_render;\n\n                                                string photoUrl = photo.pic_host + \"/large/\" + photo.pic_name;\n                                                DateTime timestamp = DateTime.UnixEpoch.AddSeconds(photo.timestamp + 8 * 3600);\n\n                                                //时间范围过滤，比设置日期早的跳过\n                                                if ((bool)settings?.EnableDatetimeRange)\n                                                {\n                                                    if (item.caption == \"微博配图\" && timestamp < settings?.StartDateTime)\n                                                    {\n                                                        AppendLog($\"翻页到截至日期{settings?.StartDateTime}，停止下载\");\n                                                        return;\n                                                    }\n                                                }\n\n                                                var invalidChar = System.IO.Path.GetInvalidFileNameChars();\n                                                var newCaption = invalidChar.Aggregate(photo.caption_render, (o, r) => (o.Replace(r.ToString(), string.Empty)));\n                                                var fileName = downloadFolder + userId + \"//\" + item.caption + \"//\"\n                                                    + timestamp.ToString(\"yyyy-MM-dd HH_mm_ss\") + newCaption + \"_\" + photoCount + \".jpg\";\n                                                Debug.WriteLine(fileName);\n                                                if (File.Exists(fileName))\n                                                {\n                                                    AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileName), MessageEnum.Warning);\n                                                    countDownloadedSkipToNextUser++;\n                                                    await Task.Delay(500);\n                                                    continue;\n                                                }\n\n                                                //传入图片/视频的名字，开始下载图片/视频\n                                                try\n                                                {\n                                                    await HttpHelper.GetAsync<AlbumDetailModel>(photoUrl, dataSource, cookie!, fileName);\n\n                                                    AppendLog(\"已完成 \" + Path.GetFileName(fileName), MessageEnum.Success);\n\n                                                    SetFileTime(fileName, timestamp);\n                                                }\n                                                catch (Exception ex)\n                                                {\n                                                    AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileName}\", MessageEnum.Error);\n                                                }\n                                            }\n                                        }\n                                        else\n                                        {\n                                            consecutiveEmptyPages++;\n                                            AppendLog($\"第 {consecutiveEmptyPages} 次遇到空页面，页码: {page}，准备尝试下一页...\", MessageEnum.Warning);\n                                            if (consecutiveEmptyPages >= 3)\n                                            {\n                                                AppendLog(\"连续 3 次遇到空页面，停止下载当前相册。\", MessageEnum.Info);\n                                                break;\n                                            }\n                                        }\n\n                                        //已存在的文件超过设置值，判定该用户下载过了\n                                        if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                        {\n                                            AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                            isSkip = true;\n                                            break;\n                                        }\n\n                                        page++;\n                                        //通过加入随机等待避免被限制。爬虫速度过快容易被系统限制(一段时间后限制会自动解除)，加入随机等待模拟人的操作，可降低被系统限制的风险。默认是每爬取1到5页随机等待6到10秒，如果仍然被限，可适当增加sleep时间\n                                        Random rd = new Random();\n                                        int rnd = rd.Next(5000, 10000);\n                                        AppendLog($\"随机等待{rnd}ms，避免爬虫速度过快被系统限制\", MessageEnum.Info);\n                                        await Task.Delay(rd.Next(5000, 10000));\n                                    }\n                                }\n                            }\n                        }\n                        //ajax获取相册方法\n                        else if (dataSource == WeiboDataSource.WeiboCom2)\n                        {\n                            string albumsUrl = $\"https://weibo.com/ajax/profile/getImageWall?uid={userId}&sinceid=0&has_album=true\";\n                            TextBlock_UID?.Dispatcher.InvokeAsync(() =>\n                            {\n                                TextBlock_UID.Text = userId;\n                            });\n\n                            var albums = await HttpHelper.GetAsync<UserAlbumModel2>(albumsUrl, dataSource, cookie!);\n                            Directory.CreateDirectory(downloadFolder + userId);\n                            if (albums != null)\n                            {\n                                foreach (var item in albums?.Data?.AlbumList!)\n                                {\n                                    if (isSkip)\n                                        break;\n\n                                    if (item.PicTitle == \"头像相册\")\n                                    {\n                                        headUrl = item.Pic;\n                                    }\n\n                                    Directory.CreateDirectory(downloadFolder + userId + \"//\" + item.PicTitle);\n\n                                    sinceId = startSinceId;\n                                    while (true)\n                                    {\n                                        if (isSkip)\n                                            break;\n\n                                        string albumUrl = $\"https://weibo.com/ajax/profile/getAlbumDetail?containerid={item.Containerid}&since_id={sinceId}\";\n                                        var photos = await HttpHelper.GetAsync<AlbumDetailModel2>(albumUrl, dataSource, cookie!, logAction: AppendLog);\n                                        if (photos != null && photos.PhotoListData2?.PhotoListItem2 != null && photos.PhotoListData2?.PhotoListItem2.Count > 0)\n                                        {\n                                            GlobalVar.gSinceId = sinceId = photos.PhotoListData2.SinceId;\n                                            foreach (var photo in photos.PhotoListData2?.PhotoListItem2!)\n                                            {\n                                                if (cancellationTokenSource.IsCancellationRequested)\n                                                {\n                                                    AppendLog($\"用户手动终止，当前页码: {page}\", MessageEnum.Info);\n                                                    return;\n                                                }\n\n                                                string photoUrl = \"https://wx4.sinaimg.cn/large/\" + photo.Pid + \".jpg\";\n\n                                                var fileName = downloadFolder + userId + \"//\" + item.PicTitle + \"//\" + photo.Pid + \".jpg\";\n                                                Debug.WriteLine(fileName);\n                                                if (File.Exists(fileName))\n                                                {\n                                                    AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileName), MessageEnum.Warning);\n                                                    countDownloadedSkipToNextUser++;\n                                                    await Task.Delay(500);\n                                                    continue;\n                                                }\n\n                                                //传入图片/视频的名字，开始下载图片/视频\n                                                try\n                                                {\n                                                    await HttpHelper.GetAsync<AlbumDetailModel>(photoUrl, dataSource, cookie!, fileName);\n\n                                                    AppendLog(\"已完成 \" + Path.GetFileName(fileName), MessageEnum.Success);\n                                                }\n                                                catch (Exception ex)\n                                                {\n                                                    AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileName}\", MessageEnum.Error);\n                                                }\n                                            }\n                                        }\n                                        else\n                                        {\n                                            break;\n                                        }\n\n                                        //已存在的文件超过设置值，判定该用户下载过了\n                                        if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                        {\n                                            AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                            isSkip = true;\n                                            break;\n                                        }\n\n                                        //通过加入随机等待避免被限制。爬虫速度过快容易被系统限制(一段时间后限制会自动解除)，加入随机等待模拟人的操作，可降低被系统限制的风险。默认是每爬取1到5页随机等待6到10秒，如果仍然被限，可适当增加sleep时间\n                                        Random rd = new Random();\n                                        int rnd = rd.Next(5000, 10000);\n                                        AppendLog($\"随机等待{rnd}ms，避免爬虫速度过快被系统限制\", MessageEnum.Info);\n                                        await Task.Delay(rd.Next(5000, 10000));\n                                    }\n                                }\n                            }\n                        }\n                        //源是weibo.cn的时候，获取的是微博时间线列表，解析html格式\n                        else if (dataSource == WeiboDataSource.WeiboCn)\n                        {\n                            page = startPage;\n                            int totalPage = -1;\n                            bool cachedUserInfo = false;\n\n                            Directory.CreateDirectory(downloadFolder + userId + \"//\" + \"微博配图\");\n                            while (true)\n                            {\n                                if (isSkip)\n                                    break;\n\n                                GlobalVar.gPage = page;\n\n                                //filter，0-全部；1-原创；2-图片\n                                //https://weibo.cn/xxxxxxxxxxxxx?page=2\n                                //https://weibo.cn/xxxxxxxxxxxxx/profile?page=2\n                                string url = $\"https://weibo.cn/{userId}/profile?page={page}&filter=1\";\n                                string text = await HttpHelper.GetAsync<string>(url, dataSource, cookie!, logAction: AppendLog);\n                                var doc = new HtmlDocument();\n                                doc.LoadHtml(text);\n\n                                //获取总页数\n                                if (totalPage == -1)\n                                {\n                                    var totalPageHtml = doc.DocumentNode.Descendants(\"input\").Where(x => x.Attributes[\"type\"]?.Value == \"hidden\").ToList();\n                                    if (totalPageHtml.Count > 0)\n                                    {\n                                        totalPage = Convert.ToInt32(totalPageHtml[totalPageHtml.Count - 1].Attributes[\"value\"].Value);\n                                        AppendLog($\"获取到{totalPage}页数据\", MessageEnum.Info);\n                                    }\n                                }\n                                //当只有一页数据的时候，totalPage获取不到。需要叠加判定doc.DocumentNode.Descendants(\"div\").Where(x => x.Attributes[\"class\"]?.Value == \"c\").ToList().Count\n                                if (totalPage == -1 && doc.DocumentNode.Descendants(\"div\").Where(x => x.Attributes[\"class\"]?.Value == \"c\").ToList().Count == 0)\n                                {\n                                    AppendLog(\"获取总页数失败，请重新登录https://weibo.cn/获取Cookie重试\", MessageEnum.Error);\n                                    return;\n                                }\n\n                                //获取用户资料\n                                if (!cachedUserInfo)\n                                {\n                                    var userInfoXmlString = doc.DocumentNode.Descendants(\"div\").Where(x => x.Attributes[\"class\"]?.Value == \"u\").ToList()?[0].OuterHtml;\n                                    var docUser = new HtmlDocument();\n                                    docUser.LoadHtml(userInfoXmlString);\n                                    string temp = docUser.DocumentNode\n                                        .Descendants(\"img\")\n                                        .FirstOrDefault(x => x.Attributes[\"alt\"]?.Value == \"头像\")\n                                        ?.Attributes[\"src\"]?.Value\n                                        ?? \"\";\n                                    nickName = docUser.DocumentNode.Descendants(\"span\").FirstOrDefault(x => x.Attributes[\"class\"]?.Value == \"ctt\")?.InnerText.Split(\"&nbsp;\")[0] ?? \"\";\n                                    if (!string.IsNullOrEmpty(nickName))\n                                        using (File.Create(downloadFolder + userId + \"//\" + nickName)) { }\n\n                                    var desc = docUser.DocumentNode.Descendants(\"div\").Where(x => x.Attributes[\"class\"]?.Value == \"tip2\").ToList()?[0].InnerText.Split(\"&nbsp;\");\n                                    string weiboDesc = string.Join(\" \", desc!);\n\n                                    headUrl = \"https://tvax2.sinaimg.cn/large/\" + Path.GetFileName(temp).Split(\"?\")[0];\n                                    var fileName = downloadFolder + userId + \"//\" + Path.GetFileName(headUrl);\n                                    //下载头像\n                                    if (!File.Exists(fileName) && temp != \"\")\n                                    {\n                                        await HttpHelper.GetAsync<AlbumDetailModel>(headUrl, dataSource, cookie!, fileName);\n                                    }\n\n                                    Image_Head?.Dispatcher.InvokeAsync(() =>\n                                    {\n                                        if (settings.ShowHeadImage && temp != \"\")\n                                        {\n                                            var bytes = File.ReadAllBytes(fileName);\n                                            MemoryStream ms = new MemoryStream(bytes);\n                                            BitmapImage bi = new BitmapImage();\n                                            bi.BeginInit();\n                                            bi.StreamSource = ms;\n                                            bi.EndInit();\n\n                                            Image_Head.ImageSource = bi;\n\n                                            //Image_Head.ImageSource = new BitmapImage(new Uri(fileName));\n                                        }\n                                        TextBlock_UID!.Text = userId;\n                                        TextBlock_NickName.Text = nickName;\n                                        TextBlock_WeiboDesc.Text = weiboDesc;\n                                    });\n\n                                    cachedUserInfo = true;\n                                }\n\n                                //获取当前页的微博\n                                var nodes = doc.DocumentNode.Descendants(\"div\").Where(x => x.Attributes[\"class\"]?.Value == \"c\").ToList();\n                                foreach (var node in nodes)\n                                {\n                                    if (isSkip)\n                                        break;\n\n                                    if (cancellationTokenSource.IsCancellationRequested)\n                                    {\n                                        AppendLog($\"用户手动终止，当前页码: {page}\", MessageEnum.Info);\n                                        return;\n                                    }\n\n                                    // 使用OuterXml属性将HtmlNode对象转换为Xml字符串\n                                    string xmlString = node.OuterHtml;\n                                    //页尾html调过解析\n                                    if (xmlString.Contains(\"设置\") && xmlString.Contains(\"图片\") && xmlString.Contains(\"条数\") && xmlString.Contains(\"隐私\"))\n                                        continue;\n\n                                    var doc1 = new HtmlDocument();\n                                    doc1.LoadHtml(xmlString);\n\n                                    //微博内容\n                                    var weiboContent = doc1.DocumentNode.Descendants(\"span\").Where(x => x.Attributes[\"class\"]?.Value == \"ctt\").ToList()[0].InnerText;\n                                    //微博来源\n                                    var temp = doc1.DocumentNode.Descendants(\"span\").Where(x => x.Attributes[\"class\"]?.Value == \"ct\").ToList()[0].InnerText;\n                                    var sourceDevice = temp.Split(\"&nbsp;\").Length > 1 ? temp.Split(\"&nbsp;\")[1] : \"\";\n                                    //发布时间\n                                    //15分钟前&nbsp;来自iPhone客户端\n                                    string pattern = @\"(\\d+分钟前|今天)\";\n                                    Regex regex = new Regex(pattern);\n                                    DateTime timestamp = DateTime.Now;\n                                    if (temp.Contains(\"分钟前\"))\n                                    {\n                                        int minute = int.Parse(temp.Split(\"&nbsp;\")[0].Replace(\"分钟前\", \"\"));\n                                        timestamp = DateTime.Now.AddMinutes(-1 * minute);\n                                    }\n                                    else\n                                    {\n                                        timestamp = DateTime.Parse(temp.Split(\"&nbsp;\")[0].Replace(\"今天\", \"\"));\n                                    }\n\n                                    //图片列表链接\n                                    string photoListUrl = string.Empty;\n                                    //视频链接\n                                    string videoUrl = string.Empty;\n                                    //转评赞\n                                    int likeCount, repostCount, commentCount;\n                                    //是单图、组图、视频\n                                    PicEnum picType = PicEnum.Picture;\n                                    var list = doc1.DocumentNode.Descendants(\"a\").ToList();\n                                    foreach (var item in list)\n                                    {\n                                        if (isSkip)\n                                            break;\n\n                                        if (item.InnerText.Contains(\"组图共\"))\n                                        {\n                                            photoListUrl = item.Attributes[\"href\"].Value;\n                                            picType = PicEnum.Pictures;\n                                        }\n                                        else if (item.Attributes[\"href\"].Value.Contains(\"s/video/show\"))\n                                        {\n                                            videoUrl = item.Attributes[\"href\"].Value.Replace(\"s/video/show\", \"s/video/object\");\n                                            picType = PicEnum.Video;\n                                        }\n                                        else if (item.InnerText.Contains(\"赞\"))\n                                        {\n                                            //likeCount = Convert.ToInt32(item.InnerText.Replace(\"赞[\", \"\").Replace(\"]\", \"\"));\n                                        }\n                                        else if (item.InnerText.Contains(\"转发\"))\n                                        {\n                                            //repostCount = Convert.ToInt32(item.InnerText.Replace(\"转发[\", \"\").Replace(\"]\", \"\"));\n                                        }\n                                        else if (item.InnerText.Contains(\"评论\"))\n                                        {\n                                            //commentCount = Convert.ToInt32(item.InnerText.Replace(\"评论[\", \"\").Replace(\"]\", \"\"));\n                                        }\n                                    }\n                                    //如果已赞，那么获取方式是下面的\n                                    try\n                                    {\n                                        //likeCount = Convert.ToInt32(doc1.DocumentNode.Descendants(\"span\").Where(x => x.Attributes[\"class\"]?.Value == \"cmt\").ToList()[0].InnerText.Replace(\"已赞[\", \"\").Replace(\"]\", \"\"));\n                                    }\n                                    catch { }\n\n                                    //获取图片列表中的每一个图片的原图超链接url\n                                    List<string> originalPics = new List<string>();\n                                    if (picType == PicEnum.Pictures)\n                                    {\n                                        text = await HttpHelper.GetAsync<string>(photoListUrl, dataSource, cookie!);\n                                        var doc2 = new HtmlDocument();\n                                        doc2.LoadHtml(text);\n                                        list = doc2.DocumentNode.Descendants(\"img\").ToList().ToList();\n                                        foreach (var item in list)\n                                        {\n                                            var photoUrl = \"https://wx4.sinaimg.cn/large/\" + Path.GetFileName(item.Attributes[\"src\"]?.Value);\n                                            originalPics.Add(photoUrl);\n                                        }\n                                    }\n                                    else if (picType == PicEnum.Picture)\n                                    {\n                                        list = doc1.DocumentNode.Descendants(\"a\").ToList();\n                                        foreach (var item in list)\n                                        {\n                                            if (item.InnerText.Contains(\"原图\"))\n                                            {\n                                                originalPics.Add(\"https://wx4.sinaimg.cn/large/\" + item.Attributes[\"href\"].Value.Split(\"u=\")[1] + \".jpg\");\n                                                break;\n                                            }\n                                        }\n                                    }\n                                    else if (picType == PicEnum.Video && settings!.EnableDownloadVideo)\n                                    {\n                                        try\n                                        {\n                                            var res = await HttpHelper.GetAsync<VideoDetailModel>(videoUrl, dataSource, cookie!);\n                                            if (res != null && res.ok == 1)\n                                            {\n                                                originalPics.Add(res?.data?.@object?.stream?.hd_url!);\n                                            }\n                                        }\n                                        catch\n                                        {\n                                            AppendLog($\"视频解析错误，原始url：{videoUrl}\", MessageEnum.Error);\n                                        }\n                                    }\n\n                                    //下载获取图片列表中的图片原图\n                                    int photoCount = 1;\n                                    foreach (var item in originalPics)\n                                    {\n                                        if (isSkip)\n                                            break;\n\n                                        if (string.IsNullOrEmpty(item))\n                                            continue;\n\n                                        //替换非法字符\n                                        var invalidChar = Path.GetInvalidFileNameChars();\n                                        var newCaption = settings!.EnableShortenName ? \"\" : invalidChar.Aggregate(weiboContent, (o, r) => (o.Replace(r.ToString(), string.Empty)));\n                                        var fileName = downloadFolder + userId + \"//\" + \"微博配图\" + \"//\"\n                                            + timestamp.ToString(\"yyyy-MM-dd HH_mm_ss\") + newCaption + \"_\" + photoCount;\n                                        //后缀名区分图片/视频\n                                        if (picType == PicEnum.Video)\n                                            fileName += \".mp4\";\n                                        else\n                                            fileName += \".jpg\";\n                                        Debug.WriteLine(fileName);\n\n                                        //已存在的文件超过设置值，判定该用户下载过了\n                                        if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                        {\n                                            AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                            isSkip = true;\n                                        }\n\n                                        //已经下载过的跳过\n                                        if (File.Exists(fileName))\n                                        {\n                                            AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileName), MessageEnum.Warning);\n                                            countDownloadedSkipToNextUser++;\n                                            await Task.Delay(500);\n                                            continue;\n                                        }\n\n                                        //传入图片/视频的名字，开始下载图片/视频\n                                        try\n                                        {\n                                            await HttpHelper.GetAsync<AlbumDetailModel>(item, dataSource, cookie!, fileName);\n\n                                            //修改文件日期时间为发博的时间\n                                            SetFileTime(fileName, timestamp);\n\n                                            AppendLog(\"已完成 \" + Path.GetFileName(fileName), MessageEnum.Success);\n                                        }\n                                        catch (Exception ex)\n                                        {\n                                            AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileName}\", MessageEnum.Error);\n                                        }\n\n                                        photoCount++;\n                                    }\n                                }\n\n                                page++;\n                                if (page > totalPage)\n                                {\n                                    break;\n                                }\n                                //通过加入随机等待避免被限制。爬虫速度过快容易被系统限制(一段时间后限制会自动解除)，加入随机等待模拟人的操作，可降低被系统限制的风险。默认是每爬取1到5页随机等待6到10秒，如果仍然被限，可适当增加sleep时间\n                                Random rd = new Random();\n                                int rnd = rd.Next(5000, 10000);\n                                AppendLog($\"随机等待{rnd}ms，避免爬虫速度过快被系统限制\", MessageEnum.Info);\n                                await Task.Delay(rnd);\n                            }\n                        }\n                        //通过m.weibo.cn移动端api获取\n                        else if (dataSource == WeiboDataSource.WeiboCnMobile)\n                        {\n                            //https://m.weibo.cn/api/container/getIndex?type=uid&value=10000000000&containerid=10760310000000000&since_id=5040866311798795\n                            sinceId = startSinceId;\n                            bool cachedUserInfo = false;\n                            string personalFolder = userId;\n                            page = startPage;\n\n                            while (true)\n                            {\n                                if (isSkip)\n                                    break;\n\n                                string url = $\"https://m.weibo.cn/api/container/getIndex?type=uid&value={userId}&containerid=107603{userId}&since_id={sinceId}&page={page}\";\n                                var res = await HttpHelper.GetAsync<WeiboCnMobileModel>(url, dataSource, cookie!, logAction: AppendLog);\n                                if (res != null && res?.Ok == 1 && res?.Data != null && res?.Data?.Cards != null && res?.Data?.Cards?.Count > 0)\n                                {\n                                    if (res?.Data?.CardlistInfo?.SinceId != null)\n                                        GlobalVar.gSinceId = sinceId = (long)res?.Data?.CardlistInfo?.SinceId!;\n                                    AppendLog($\"获取到{res?.Data?.CardlistInfo?.Total}条数据，正在下载第{page}页，SinceId: {sinceId}\", MessageEnum.Info);\n\n                                    if (res?.Data?.Cards?[res.Data.Cards.Count - 1]?.Mblog?.User?.ScreenName == null)\n                                        return;\n\n                                    //获取用户资料\n                                    if (!cachedUserInfo)\n                                    {\n                                        nickName = res?.Data?.Cards?[res.Data.Cards.Count - 1]?.Mblog?.User?.ScreenName!;\n                                        personalFolder = $\"{nickName}({userId})\";\n                                        Directory.CreateDirectory(downloadFolder + \"//\" + personalFolder);\n                                        using (File.Create(downloadFolder + \"//\" + personalFolder + \"//\" + nickName)) { }\n\n                                        headUrl = \"https://tvax2.sinaimg.cn/large/\" + Path.GetFileName(res?.Data?.Cards?[res.Data.Cards.Count - 1]?.Mblog?.User?.AvatarHd!).Split(\"?\")[0];\n                                        var fileName = downloadFolder + \"//\" + personalFolder + \"//\" + Path.GetFileName(headUrl);\n                                        bool downloadHeadImageSuccess = true;\n                                        //下载头像\n                                        if (!File.Exists(fileName))\n                                        {\n                                            try\n                                            {\n                                                await HttpHelper.GetAsync<AlbumDetailModel>(headUrl, dataSource, cookie!, fileName);\n                                            }\n                                            catch\n                                            {\n                                                downloadHeadImageSuccess = false;\n                                                AppendLog($\"头像下载失败\", MessageEnum.Warning);\n                                            }\n                                        }\n\n\n                                        Image_Head?.Dispatcher.InvokeAsync(() =>\n                                        {\n                                            if (settings.ShowHeadImage && downloadHeadImageSuccess)\n                                            {\n                                                var bytes = File.ReadAllBytes(fileName);\n                                                MemoryStream ms = new MemoryStream(bytes);\n                                                BitmapImage bi = new BitmapImage();\n                                                bi.BeginInit();\n                                                bi.StreamSource = ms;\n                                                bi.EndInit();\n\n                                                Image_Head.ImageSource = bi;\n\n                                                //Image_Head.ImageSource = new BitmapImage(new Uri(fileName));\n                                            }\n                                            TextBlock_UID!.Text = userId;\n                                            TextBlock_NickName.Text = nickName;\n                                            TextBlock_WeiboDesc.Text = res?.Data?.Cards?[res.Data.Cards.Count - 1]?.Mblog?.User?.Description!;\n                                        });\n\n                                        cachedUserInfo = true;\n                                    }\n\n                                    //获取图片列表中的每一个图片的原图超链接url\n                                    List<string> originalPics = new List<string>();\n                                    List<string> originalVideos = new List<string>();\n                                    List<string> originalLivePhotos = new List<string>();\n                                    string weiboContent = \"\";\n                                    DateTime timestamp = DateTime.Now;\n\n                                    foreach (var card in res?.Data?.Cards!)\n                                    {\n                                        if (isSkip)\n                                            break;\n                                        //9是微博，RetweetedStatus是转发\n                                        if (card?.CardType != 9 || card?.Mblog?.RetweetedStatus != null)\n                                            continue;\n\n                                        if (cancellationTokenSource.IsCancellationRequested)\n                                        {\n                                            AppendLog($\"用户手动终止，当前页码: {page}, SinceID: {sinceId}\", MessageEnum.Info);\n                                            return;\n                                        }\n                                        originalPics.Clear();\n                                        originalVideos.Clear();\n                                        originalLivePhotos.Clear();\n                                        //weiboContent = card?.Mblog?.Text!;\n                                        // 使用正则表达式去除 <a> 和 <span> 标签及其内容\n                                        string result = Regex.Replace(card?.Mblog?.Text!, @\"<a.*?>.*?</a>|<span.*?>.*?</span>\", string.Empty);\n                                        // 去除其他不需要的标签（如 <br />）\n                                        weiboContent = Regex.Replace(result, @\"<.*?>\", string.Empty);\n\n\n                                        string format = \"ddd MMM dd HH:mm:ss K yyyy\"; // 定义日期格式\n\n                                        timestamp = DateTime.ParseExact(card?.Mblog?.CreatedAt!, format, System.Globalization.CultureInfo.InvariantCulture);\n\n                                        //时间范围过滤，比设置日期早的跳过\n                                        if ((bool)settings?.EnableDatetimeRange)\n                                        {\n                                            if (card?.ProfileTypeId == \"proweibotop_\" && timestamp < settings?.StartDateTime)\n                                                continue;\n                                            else if (card?.ProfileTypeId == \"proweibotop_\" && timestamp >= settings?.StartDateTime)\n                                            {\n\n                                            }\n                                            else if (timestamp < settings?.StartDateTime)\n                                            {\n                                                AppendLog($\"翻页到截至日期{settings?.StartDateTime}，停止下载\");\n                                                return;\n                                            }\n                                        }\n                                        Debug.WriteLine(card?.Mblog?.Text);\n\n                                        //如果PicNum大于PicIds.Count，那么图片可能超过9张图。\n                                        if (card?.Mblog?.PicIds != null && (bool)card?.Mblog?.PicIds?.Any()!)\n                                        {\n                                            if (card?.Mblog?.PicIds!.Count == card?.Mblog?.PicNum)\n                                            {\n                                                foreach (var item in card?.Mblog?.PicIds!)\n                                                {\n                                                    var photoUrl = \"https://wx4.sinaimg.cn/large/\" + Path.GetFileName(item) + \".jpg\";\n                                                    originalPics.Add(photoUrl);\n                                                }\n                                            }\n                                            //多于9图\n                                            else\n                                            {\n                                                var picIds = await WeiboMidHelper.GetImageIdsByMidAsync(card?.Mblog?.Mid!, settings?.WeiboComCookie);\n                                                foreach (var item in picIds)\n                                                {\n                                                    var photoUrl = \"https://wx4.sinaimg.cn/large/\" + Path.GetFileName(item) + \".jpg\";\n                                                    originalPics.Add(photoUrl);\n                                                }\n                                            }\n                                        }\n                                        if (card?.Mblog?.LivePhoto != null && (bool)(card?.Mblog?.LivePhoto?.Any()!))\n                                        {\n                                            foreach (var item in card?.Mblog?.LivePhoto!)\n                                            {\n                                                originalLivePhotos.Add(item);\n                                            }\n                                        }\n                                        //选最高清晰度\n                                        if (card?.Mblog?.PageInfo?.Urls?.Mp48kMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp48kMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp44kMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp44kMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp42kMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp42kMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp41080pMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp41080pMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp4720pMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp4720pMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp4HDMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp4HDMp4!);\n                                        }\n                                        else if (card?.Mblog?.PageInfo?.Urls?.Mp4LDMp4 != null)\n                                        {\n                                            originalVideos.Add(card?.Mblog?.PageInfo?.Urls?.Mp4LDMp4!);\n                                        }\n\n                                        //替换非法字符\n                                        var invalidChar = Path.GetInvalidFileNameChars();\n                                        var newCaption = settings!.EnableShortenName ? \"\" : invalidChar.Aggregate(weiboContent, (o, r) => (o.Replace(r.ToString(), string.Empty)));\n                                        var fileName = downloadFolder + \"//\" + personalFolder + \"//\" + \"//\"\n                                            + timestamp.ToString(\"yyyy-MM-dd HH_mm_ss\") + newCaption;\n                                        Debug.WriteLine(fileName);\n\n                                        int id = 1;\n                                        //下载获取图片列表中的图片原图\n                                        foreach (var item in originalPics)\n                                        {\n                                            if (isSkip)\n                                                break;\n\n                                            if (string.IsNullOrEmpty(item))\n                                                continue;\n                                            var fileNamee = fileName + $\"_{id}.jpg\";\n                                            //已存在的文件超过设置值，判定该用户下载过了\n                                            if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                            {\n                                                AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                                isSkip = true;\n                                            }\n\n                                            //已经下载过的跳过\n                                            if (File.Exists(fileNamee))\n                                            {\n                                                AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileNamee), MessageEnum.Warning);\n                                                countDownloadedSkipToNextUser++;\n                                                await Task.Delay(500);\n                                                continue;\n                                            }\n\n                                            //传入图片/视频的名字，开始下载图片/视频\n                                            try\n                                            {\n                                                await HttpHelper.GetAsync<AlbumDetailModel>(item, dataSource, cookie!, fileNamee);\n\n                                                //修改文件日期时间为发博的时间\n                                                SetFileTime(fileNamee, timestamp);\n\n                                                AppendLog(\"已完成 \" + Path.GetFileName(fileNamee), MessageEnum.Success);\n                                            }\n                                            catch (Exception ex)\n                                            {\n                                                AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileNamee}\", MessageEnum.Error);\n                                            }\n                                            id++;\n                                        }\n                                        if (settings!.EnableDownloadVideo)\n                                        {\n                                            foreach (var item in originalVideos)\n                                            {\n                                                if (isSkip)\n                                                    break;\n\n                                                if (string.IsNullOrEmpty(item))\n                                                    continue;\n                                                var fileNamee = fileName + $\"_{id}.mp4\";\n                                                //已存在的文件超过设置值，判定该用户下载过了\n                                                if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                                {\n                                                    AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                                    isSkip = true;\n                                                }\n\n                                                //已经下载过的跳过\n                                                if (File.Exists(fileNamee))\n                                                {\n                                                    AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileNamee), MessageEnum.Warning);\n                                                    countDownloadedSkipToNextUser++;\n                                                    await Task.Delay(500);\n                                                    continue;\n                                                }\n\n                                                //传入图片/视频的名字，开始下载图片/视频\n                                                try\n                                                {\n                                                    await HttpHelper.GetAsync<AlbumDetailModel>(item, dataSource, cookie!, fileNamee);\n\n                                                    //修改文件日期时间为发博的时间\n                                                    SetFileTime(fileNamee, timestamp);\n\n                                                    AppendLog(\"已完成 \" + Path.GetFileName(fileNamee), MessageEnum.Success);\n                                                }\n                                                catch (Exception ex)\n                                                {\n                                                    AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileNamee}\", MessageEnum.Error);\n                                                }\n                                                id++;\n                                            }\n                                        }\n                                        if (settings!.EnableDownloadLivePhoto)\n                                        {\n                                            foreach (var item in originalLivePhotos)\n                                            {\n                                                if (isSkip)\n                                                    break;\n\n                                                if (string.IsNullOrEmpty(item))\n                                                    continue;\n                                                var fileNamee = fileName + $\"_{id}.mov\";\n                                                //已存在的文件超过设置值，判定该用户下载过了\n                                                if (settings!.CountDownloadedSkipToNextUser > 0 && countDownloadedSkipToNextUser > settings.CountDownloadedSkipToNextUser)\n                                                {\n                                                    AppendLog($\"已存在的文件{countDownloadedSkipToNextUser}超过设置值{settings.CountDownloadedSkipToNextUser}，跳到下一个用户\", MessageEnum.Info);\n                                                    isSkip = true;\n                                                }\n\n                                                //已经下载过的跳过\n                                                if (File.Exists(fileNamee))\n                                                {\n                                                    AppendLog(\"文件已存在，跳过下载\" + Path.GetFullPath(fileNamee), MessageEnum.Warning);\n                                                    countDownloadedSkipToNextUser++;\n                                                    await Task.Delay(500);\n                                                    continue;\n                                                }\n\n                                                //传入图片/视频的名字，开始下载图片/视频\n                                                try\n                                                {\n                                                    await HttpHelper.GetAsync<AlbumDetailModel>(item, dataSource, cookie!, fileNamee);\n\n                                                    //修改文件日期时间为发博的时间\n                                                    SetFileTime(fileNamee, timestamp);\n\n                                                    AppendLog(\"已完成 \" + Path.GetFileName(fileNamee), MessageEnum.Success);\n                                                }\n                                                catch (Exception ex)\n                                                {\n                                                    AppendLog($\"文件下载失败，原始url：{item}，下载路径{fileNamee}\", MessageEnum.Error);\n                                                }\n                                                id++;\n                                            }\n                                        }\n                                    }\n\n                                    page++;\n                                }\n                                else\n                                {\n                                    break;\n                                }\n\n                                //通过加入随机等待避免被限制。爬虫速度过快容易被系统限制(一段时间后限制会自动解除)，加入随机等待模拟人的操作，可降低被系统限制的风险。默认是每爬取1到5页随机等待6到10秒，如果仍然被限，可适当增加sleep时间\n                                Random rd = new Random();\n                                int rnd = rd.Next(5000, 10000);\n                                AppendLog($\"随机等待{rnd}ms，避免爬虫速度过快被系统限制\", MessageEnum.Info);\n                                await Task.Delay(rnd);\n                            }\n                        }\n\n\n                        //单个用户结束下载\n                        if (!string.IsNullOrEmpty(nickName))\n                        {\n                            string info = $\"{nickName} <a href=\\\"//weibo.com/u/{userId}\\\">{userId}{nickName}</a>于{DateTime.Now.ToString(\"HH:mm:ss\")}结束下载，程序版本V{currentVersion}<img src=\\\"{headUrl}\\\">\";\n                            await PushPlusHelper.SendMessage(settings?.PushPlusToken!, \"微博相册下载\", info);\n                            SentrySdk.CaptureMessage(info);\n\n                            AddToUserIdList(userId, nickName);\n\n                            AppendLog(info, MessageEnum.Info);\n                        }\n                        else\n                        {\n                            AppendLog($\"未能获取到用户 {userId} 的基本信息（如昵称），下载失败。最后尝试位置 Page: {page}, SinceID: {sinceId}。可能原因：用户不存在、暂无微博内容、Cookie失效或网络问题。\", MessageEnum.Error);\n                        }\n                    }\n                });\n\n                //所有用户结束下载\n                AppendLog(\"结束下载。\", MessageEnum.Info);\n            }\n            catch (Exception ex)\n            {\n                string msg = $\"遇到错误，uid: {GlobalVar.gId}，DataSource: {dataSource}，Page:{GlobalVar.gPage}，SinceId: {GlobalVar.gSinceId}，\" + ex.ToString() + \"，请稍后重试。\";\n                AppendLog(msg, MessageEnum.Error);\n                SentrySdk.CaptureMessage(msg, SentryLevel.Error);\n            }\n            finally\n            {\n                isDownloading = false;\n                Application.Current.Dispatcher.Invoke(() =>\n                {\n                    tbDownload.Text = \"开始下载\";\n                });\n            }\n        }\n\n        public void AppendLog(string text, MessageEnum messageEnum = MessageEnum.Info)\n        {\n            string msg = $\"{DateTime.Now} {text}\";\n            Debug.WriteLine(msg);\n\n            ListView_Messages?.Dispatcher.InvokeAsync(() =>\n            {\n                Messages.Insert(0, new MessageModel()\n                {\n                    Time = DateTime.Now.ToString(\"HH:mm:ss\"),\n                    Message = text,\n                    MessageType = messageEnum\n                });\n            });\n        }\n\n        private void AddToUserIdList(string userId, string nickName)\n        {\n            //不在列表的，才写入文件\n            if (!uids.Contains(userId))\n            {\n                uids.Add(userId);\n                File.AppendAllText(\"uidList.txt\", Environment.NewLine + $\"{userId},{nickName}\");\n            }\n        }\n\n        private void InitUidsData()\n        {\n            //配置文件不存在就创建\n            if (!File.Exists(\"uidList.txt\"))\n            {\n                File.WriteAllText(\"uidList.txt\", \"//可以是多用户，换行隔开。\\r\\n//行内用英文逗号隔开，用户id(必填),用户名(可选)\\r\\n\");\n            }\n\n            uids.Clear();\n            //文件中可以是多用户，换行隔开。行内用英文逗号隔开，用户id(必填),用户名(可选)\n            var lines = File.ReadAllLines(\"uidList.txt\");\n            foreach (var line in lines)\n            {\n                if (line.StartsWith(\"//\") || string.IsNullOrEmpty(line.Trim()))\n                    continue;\n\n                var temp = line.Split(',');\n                uids.Add(temp[0]);\n            }\n\n        }\n\n        private void InitSettingsData()\n        {\n            if (!File.Exists(\"Settings.json\"))\n            {\n                File.WriteAllText(\"Settings.json\", JsonConvert.SerializeObject(new SettingsModel(), Formatting.Indented));\n            }\n\n            string settingsContent = File.ReadAllText(\"Settings.json\");\n            settings = JsonConvert.DeserializeObject<SettingsModel>(settingsContent);\n            if (settings == null)\n            {\n                AppendLog(\"Settings.json文件缺失，请重新下载程序\", MessageEnum.Error);\n                return;\n            }\n            if (string.IsNullOrEmpty(settings.PushPlusToken))\n            {\n                AppendLog(\"没有检测到PushPlus token，程序将不会推送消息到微信。如需推送，请登录https://www.pushplus.plus/设置\", MessageEnum.Info);\n            }\n            if (dataSource == WeiboDataSource.WeiboCn || dataSource == WeiboDataSource.WeiboCnMobile)\n            {\n                cookie = settings.WeiboCnCookie;\n            }\n            else\n            {\n                cookie = settings.WeiboComCookie;\n            }\n            if (string.IsNullOrEmpty(cookie))\n            {\n                AppendLog(\"没有检测到cookie，程序将无法抓取数据，请在设置里面扫码获取\", MessageEnum.Error);\n                return;\n            }\n            if (!settings.EnableCrontab)\n            {\n                AppendLog(\"没有开启Crontab定时任务，程序将不会自动执行\", MessageEnum.Info);\n            }\n            if (settings.EnableCrontab && !string.IsNullOrEmpty(settings.Crontab))\n            {\n                AppendLog($\"已开启Crontab定时任务，{CronExpressionDescriptor.ExpressionDescriptor.GetDescription(settings.Crontab)}\", MessageEnum.Info);\n            }\n        }\n\n        private void SetFileTime(string filename, DateTime timestamp)\n        {\n            File.SetCreationTime(filename, timestamp);\n            File.SetLastWriteTime(filename, timestamp);\n            File.SetLastAccessTime(filename, timestamp);\n        }\n\n        #region UI操作\n        private async void StartDownLoad(object sender, RoutedEventArgs e)\n        {\n            if (tbDownload.Text == \"开始下载\")\n            {\n                if (isDownloading)\n                {\n                    AppendLog(\"正在执行下载任务\");\n                    return;\n                }\n\n                isDownloading = true;\n                tbDownload.Text = \"停止下载\";\n\n                string pattern = @\"\\d+\";  // 匹配一个或多个数字\n                Match match = Regex.Match(TextBox_WeiboId.Text.Trim(), pattern);\n\n                // 读取用户输入的起始页码和SinceId\n                int.TryParse(TextBox_StartPage.Text, out int startPage);\n                long.TryParse(TextBox_StartSinceId.Text, out long startSinceId);\n\n                if (match.Success)\n                {\n                    await Start(match.Value, startPage, startSinceId);\n                }\n                else\n                {\n                    AppendLog(\"没有找到微博账号\", MessageEnum.Warning);\n                    isDownloading = false;\n                    tbDownload.Text = \"开始下载\";\n                }\n            }\n            else\n            {\n                isDownloading = false;\n                tbDownload.Text = \"开始下载\";\n                cancellationTokenSource?.Cancel();\n            }\n        }\n\n        private void StopDownLoad(object sender, RoutedEventArgs e)\n        {\n            cancellationTokenSource?.Cancel();\n        }\n\n        private async void BatchDownLoad(object sender, RoutedEventArgs e)\n        {\n            if (isDownloading)\n            {\n                AppendLog(\"正在执行下载任务\");\n                return;\n            }\n\n            isDownloading = true;\n            AppendLog(DateTime.Now.ToString(\"yyyy-MM-dd HH-mm-ss\") + \"开启批量任务\");\n\n            foreach (var item in uids)\n            {\n                await Start(item.Trim());\n                AppendLog(\"等待60s，下载下一个用户相册数据\");\n                await Task.Delay(60 * 1000);\n            }\n\n            AppendLog(DateTime.Now.ToString(\"yyyy-MM-dd HH-mm-ss\") + \"结束批量任务\");\n            isDownloading = false;\n        }\n\n        private void ListView_CopyLog(object sender, RoutedEventArgs e)\n        {\n            if (ListView_Messages.SelectedItem != null)\n                Clipboard.SetText((ListView_Messages.SelectedItem! as MessageModel)!.Message);\n        }\n\n        private void ListView_ClearLog(object sender, RoutedEventArgs e)\n        {\n            Messages.Clear();\n        }\n\n        private void ListView_ExportLog(object sender, RoutedEventArgs e)\n        {\n            var dialog = new Microsoft.Win32.SaveFileDialog();\n            dialog.FileName = $\"{GlobalVar.gId}-log\";\n            dialog.DefaultExt = \".txt\";\n            dialog.Filter = \"下载日志 (.txt)|*.txt\";\n\n            bool? result = dialog.ShowDialog();\n\n            if (result == true)\n            {\n                StringBuilder sb = new StringBuilder();\n                foreach (var message in Messages)\n                {\n                    string line = $\"{message.Time},{message.Message},{message.MessageType}\";\n                    sb.AppendLine(line);\n                }\n\n                File.WriteAllText(dialog.FileName, sb.ToString());\n            }\n        }\n\n        private void ListView_OpenDownloadFolder(object sender, RoutedEventArgs e)\n        {\n            Process.Start(\"explorer.exe\", Path.GetFullPath(downloadFolder));\n        }\n\n        private async void TextBox_WeiboId_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)\n        {\n            if (e.Key == System.Windows.Input.Key.Enter)\n            {\n                StartDownLoad(null, null);\n            }\n        }\n\n        private void OpenSettings(object sender, RoutedEventArgs e)\n        {\n            var set = new SettingsWindow();\n            set.Owner = this;\n            set.ShowDialog();\n            AppendLog(\"设置已更新\");\n            InitSettingsData();\n        }\n\n        private void MicaWindow_SizeChanged(object sender, SizeChangedEventArgs e)\n        {\n            Border_Head.Width = Border_Head.Height = Column_LeftGrid.ActualWidth * 0.8;\n            Border_Head.CornerRadius = new CornerRadius(Column_LeftGrid.ActualWidth * 0.8);\n        }\n        #endregion\n\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/AlbumDetailModel.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public class AlbumDetailModel\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public string result { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int code { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string msg { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int timestamp { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public PhotoListData data { get; set; }\n    }\n\n    public class PhotoListCount\n    {\n        /// <summary>\n        /// 评论数\n        /// </summary>\n        public int comments { get; set; }\n        /// <summary>\n        /// 查数\n        /// </summary>\n        public int clicks { get; set; }\n        /// <summary>\n        /// 转发数\n        /// </summary>\n        public int retweets { get; set; }\n        /// <summary>\n        /// 点赞数\n        /// </summary>\n        public int likes { get; set; }\n    }\n\n    public class PhotoListItem\n    {\n        /// <summary>\n        /// 相册ID\n        /// </summary>\n        public string? album_id { get; set; }\n        /// <summary>\n        /// 推文内容\n        /// </summary>\n        public string caption { get; set; }\n        /// <summary>\n        /// 推文内容\n        /// </summary>\n        public string caption_render { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public PhotoListCount count { get; set; }\n        /// <summary>\n        /// 创建日期\n        /// </summary>\n        public string created_at { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int @type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string @from { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_favorited { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_liked { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string oid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string photo_id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string pic_host { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string pic_name { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string pic_pid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int pic_type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string source { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string tags { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int timestamp { get; set; }\n        /// <summary>\n        /// 用户id\n        /// </summary>\n        public long uid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string updated_at { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int property { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int? visible_type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string mid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_private { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string feed_id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public double? latitude { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public double? longitude { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_paid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int mblog_vip_type { get; set; }\n    }\n\n    public class PhotoListData\n    {\n        /// <summary>\n        /// 相册ID\n        /// </summary>\n        public string? album_id { get; set; }\n        /// <summary>\n        /// 照片总数\n        /// </summary>\n        public int total { get; set; }\n        /// <summary>\n        /// 照片列表，分页\n        /// </summary>\n        public List<PhotoListItem>? photo_list { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/AlbumDetailModel2.cs",
    "content": "﻿using Newtonsoft.Json;\nusing System.Collections.Generic;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public partial class AlbumDetailModel2\n    {\n        [JsonProperty(\"data\")]\n        public PhotoListData2? PhotoListData2 { get; set; }\n\n        [JsonProperty(\"ok\")]\n        public long Ok { get; set; }\n    }\n\n    public partial class PhotoListData2\n    {\n        [JsonProperty(\"type\")]\n        public string? Type { get; set; }\n\n        [JsonProperty(\"list\")]\n        public List<PhotoListItem2>? PhotoListItem2 { get; set; }\n\n        [JsonProperty(\"since_id\")]\n        public long SinceId { get; set; }\n    }\n\n    public partial class PhotoListItem2\n    {\n        [JsonProperty(\"pid\")]\n        public string? Pid { get; set; }\n\n        [JsonProperty(\"mid\")]\n        public string? Mid { get; set; }\n\n        [JsonProperty(\"is_paid\")]\n        public bool IsPaid { get; set; }\n\n        [JsonProperty(\"timeline_month\")]\n        public string? TimelineMonth { get; set; }\n\n        [JsonProperty(\"timeline_year\")]\n        public string? TimelineYear { get; set; }\n\n        [JsonProperty(\"object_id\")]\n        public string? ObjectId { get; set; }\n\n        [JsonProperty(\"type\")]\n        public string? Type { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/MessageModel.cs",
    "content": "﻿using System;\nusing WeiboAlbumDownloader.Enums;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public class MessageModel\n    {\n        public string? Message { get; set; }\n        public string? Time { get; set; }\n        public MessageEnum MessageType { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/SettingsModel.cs",
    "content": "﻿using System;\nusing WeiboAlbumDownloader.Enums;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public class SettingsModel\n    {\n        //数据源\n        public WeiboDataSource DataSource { get; set; } = WeiboDataSource.WeiboCnMobile;\n        //是否显示作者头像\n        public bool ShowHeadImage { get; set; } = true;\n        //weibo.cn cookie\n        public string? WeiboCnCookie { get; set; }\n        //weibo.com cookie\n        public string? WeiboComCookie { get; set; }\n        //推送到微信，填了就会发送\n        public string? PushPlusToken { get; set; }\n        //是否开启Crontab定时任务\n        public bool EnableCrontab { get; set; } = true;\n        //Crontab定时任务\n        public string? Crontab { get; set; } = \"14 2 * * *\";\n        //用来跳过到下一个uid的计数。如果当前uid下载的时候已存在文件超过此计数，则判定下载过了。-1表示不判定\n        public int CountDownloadedSkipToNextUser { get; set; } = 20;\n        //是否开启时间范围\n        public bool EnableDatetimeRange { get; set; } = false;\n        public DateTime? StartDateTime { get; set; }\n        //开启下载视频功能，默认开启\n        public bool EnableDownloadVideo { get; set; } = true;\n        //开启下载LivePhoto\n        public bool EnableDownloadLivePhoto { get; set; } = true;\n        //启用后图片仅以日期+编号命名，文件名中不在包含博文内容\n        public bool EnableShortenName { get; set; } = false;\n\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/UserAlbumModel.cs",
    "content": "﻿using System.Collections.Generic;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    internal class UserAlbumModel\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public string result { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int code { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string msg { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int timestamp { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public AlbumData data { get; set; }\n    }\n    public class AlbumCount\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public int photos { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int likes { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int comments { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int retweets { get; set; }\n    }\n\n    public class AlbumListItem\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public string album_id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string uid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string property { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int status { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string source { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string album_order { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string created_at { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string usort { get; set; }\n        /// <summary>\n        /// 头像相册\n        /// </summary>\n        public string caption { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string description { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string cover_pic { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string cover_photo_id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string question { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string answer { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string updated_at { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string timestamp { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int updated_at_int { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_favorited { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string is_private { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string thumb120_pic { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string thumb300_pic { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string sq612_pic { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public AlbumCount count { get; set; }\n    }\n\n    public class AlbumData\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public int total { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public List<AlbumListItem> album_list { get; set; }\n    }\n\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/UserAlbumModel2.cs",
    "content": "﻿using Newtonsoft.Json;\nusing System;\nusing System.Collections.Generic;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public partial class UserAlbumModel2\n    {\n        [JsonProperty(\"data\")]\n        public AlbumData2 Data { get; set; }\n\n        [JsonProperty(\"bottom_tips_visible\")]\n        public bool BottomTipsVisible { get; set; }\n\n        [JsonProperty(\"bottom_tips_text\")]\n        public string BottomTipsText { get; set; }\n\n        [JsonProperty(\"ok\")]\n        public long Ok { get; set; }\n    }\n\n    public partial class AlbumData2\n    {\n        [JsonProperty(\"album_list\")]\n        public List<AlbumList2> AlbumList { get; set; }\n\n        [JsonProperty(\"album_since_id\")]\n        public long AlbumSinceId { get; set; }\n\n        [JsonProperty(\"since_id\")]\n        public string SinceId { get; set; }\n\n        [JsonProperty(\"list\")]\n        public List<List2> List { get; set; }\n    }\n\n    public partial class AlbumList2\n    {\n        [JsonProperty(\"pic_title\")]\n        public string PicTitle { get; set; }\n\n        [JsonProperty(\"containerid\")]\n        public string Containerid { get; set; }\n\n        [JsonProperty(\"pic\")]\n        public string Pic { get; set; }\n    }\n\n    public partial class List2\n    {\n        [JsonProperty(\"pid\")]\n        public string Pid { get; set; }\n\n        [JsonProperty(\"mid\")]\n        public string Mid { get; set; }\n\n        [JsonProperty(\"is_paid\")]\n        public bool IsPaid { get; set; }\n\n        [JsonProperty(\"timeline_month\")]\n        public string TimelineMonth { get; set; }\n\n        [JsonProperty(\"timeline_year\")]\n        public string TimelineYear { get; set; }\n\n        [JsonProperty(\"object_id\")]\n        public string ObjectId { get; set; }\n\n        [JsonProperty(\"type\")]\n        public string Type { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/VideoDetailModel.cs",
    "content": "﻿namespace WeiboAlbumDownloader.Models\n{\n    public class VideoDetailModel\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public int ok { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public VideoData data { get; set; }\n    }\n\n    public class VideoData\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public string object_id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string object_type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public @object @object { get; set; }\n    }\n\n    public class @object\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public string summary { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public Author author { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public @Stream stream { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string created_at { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public Image image { get; set; }\n    }\n\n    public class Author\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public long id { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string screen_name { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string profile_image_url { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string profile_url { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int statuses_count { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string verified { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int verified_type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string close_blue_v { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string description { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string gender { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int mbtype { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int svip { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int urank { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int mbrank { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string follow_me { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string following { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int follow_count { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string followers_count { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string followers_count_str { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string cover_image_phone { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string avatar_hd { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string like { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string like_me { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string special_follow { get; set; }\n    }\n\n    public class @Stream\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public double duration { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string format { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int width { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string hd_url { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string url { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int height { get; set; }\n    }\n\n    public class Image\n    {\n        /// <summary>\n        /// \n        /// </summary>\n        public int width { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string pid { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int source { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int is_self_cover { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int type { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public string url { get; set; }\n        /// <summary>\n        /// \n        /// </summary>\n        public int height { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/Models/WeiboCnMobileModel.cs",
    "content": "﻿using Newtonsoft.Json;\nusing System;\nusing System.Collections.Generic;\n\nnamespace WeiboAlbumDownloader.Models\n{\n    public partial class WeiboCnMobileModel\n    {\n        [JsonProperty(\"ok\")]\n        public long? Ok { get; set; }\n\n        [JsonProperty(\"data\")]\n        public Data? Data { get; set; }\n    }\n\n    public partial class Data\n    {\n        [JsonProperty(\"cardlistInfo\")]\n        public CardlistInfo? CardlistInfo { get; set; }\n\n        [JsonProperty(\"cards\")]\n        public List<Card>? Cards { get; set; }\n\n        [JsonProperty(\"scheme\")]\n        public string? Scheme { get; set; }\n\n        [JsonProperty(\"showAppTips\")]\n        public long? ShowAppTips { get; set; }\n    }\n\n    public partial class CardlistInfo\n    {\n        [JsonProperty(\"containerid\")]\n        public string? Containerid { get; set; }\n\n        [JsonProperty(\"v_p\")]\n        public long? VP { get; set; }\n\n        [JsonProperty(\"show_style\")]\n        public long? ShowStyle { get; set; }\n\n        [JsonProperty(\"total\")]\n        public long? Total { get; set; }\n\n        [JsonProperty(\"autoLoadMoreIndex\")]\n        public long? AutoLoadMoreIndex { get; set; }\n\n        [JsonProperty(\"since_id\")]\n        public long? SinceId { get; set; }\n    }\n\n    public partial class Card\n    {\n        [JsonProperty(\"card_type\")]\n        public long? CardType { get; set; }\n\n        [JsonProperty(\"profile_type_id\")]\n        public string? ProfileTypeId { get; set; }\n\n        [JsonProperty(\"itemid\")]\n        public string? Itemid { get; set; }\n\n        [JsonProperty(\"scheme\")]\n        public Uri? Scheme { get; set; }\n\n        [JsonProperty(\"mblog\")]\n        public Mblog? Mblog { get; set; }\n    }\n\n    public partial class Mblog\n    {\n        [JsonProperty(\"visible\")]\n        public Visible? Visible { get; set; }\n\n        [JsonProperty(\"created_at\")]\n        public string? CreatedAt { get; set; }\n\n        [JsonProperty(\"id\")]\n        public string? Id { get; set; }\n\n        [JsonProperty(\"mid\")]\n        public string? Mid { get; set; }\n\n        [JsonProperty(\"can_edit\")]\n        public bool? CanEdit { get; set; }\n\n        [JsonProperty(\"text\")]\n        public string? Text { get; set; }\n\n        [JsonProperty(\"textLength\")]\n        public long? TextLength { get; set; }\n\n        [JsonProperty(\"source\")]\n        public string? Source { get; set; }\n\n        [JsonProperty(\"favorited\")]\n        public bool? Favorited { get; set; }\n\n        [JsonProperty(\"pic_ids\")]\n        public List<string>? PicIds { get; set; }\n\n        [JsonProperty(\"thumbnail_pic\")]\n        public string? ThumbnailPic { get; set; }\n\n        [JsonProperty(\"bmiddle_pic\")]\n        public string? BmiddlePic { get; set; }\n\n        [JsonProperty(\"original_pic\")]\n        public string? OriginalPic { get; set; }\n\n        [JsonProperty(\"is_paid\")]\n        public bool? IsPaid { get; set; }\n\n        [JsonProperty(\"mblog_vip_type\")]\n        public long? MblogVipType { get; set; }\n\n        [JsonProperty(\"user\")]\n        public User? User { get; set; }\n\n        [JsonProperty(\"retweeted_status\")]\n        public RetweetedStatus? RetweetedStatus { get; set; }\n\n        [JsonProperty(\"reposts_count\")]\n        public long? RepostsCount { get; set; }\n\n        [JsonProperty(\"comments_count\")]\n        public long? CommentsCount { get; set; }\n\n        [JsonProperty(\"reprint_cmt_count\")]\n        public long? ReprintCmtCount { get; set; }\n\n        [JsonProperty(\"attitudes_count\")]\n        public long? AttitudesCount { get; set; }\n\n        [JsonProperty(\"mixed_count\")]\n        public long? MixedCount { get; set; }\n\n        [JsonProperty(\"pending_approval_count\")]\n        public long? PendingApprovalCount { get; set; }\n\n        [JsonProperty(\"isLongText\")]\n        public bool? IsLongText { get; set; }\n\n        [JsonProperty(\"show_mlevel\")]\n        public long? ShowMlevel { get; set; }\n\n        [JsonProperty(\"darwin_tags\")]\n        public List<DarwinTag>? DarwinTags { get; set; }\n\n        [JsonProperty(\"ad_marked\")]\n        public bool? AdMarked { get; set; }\n\n        [JsonProperty(\"mblogtype\")]\n        public long? Mblogtype { get; set; }\n\n        [JsonProperty(\"item_category\")]\n        public string? ItemCategory { get; set; }\n\n        [JsonProperty(\"rid\")]\n        public string? Rid { get; set; }\n\n        [JsonProperty(\"extern_safe\")]\n        public long? ExternSafe { get; set; }\n\n        [JsonProperty(\"number_display_strategy\")]\n        public NumberDisplayStrategy? NumberDisplayStrategy { get; set; }\n\n        [JsonProperty(\"content_auth\")]\n        public long? ContentAuth { get; set; }\n\n        [JsonProperty(\"is_show_mixed\")]\n        public bool? IsShowMixed { get; set; }\n\n        [JsonProperty(\"comment_manage_info\")]\n        public CommentManageInfo? CommentManageInfo { get; set; }\n\n        [JsonProperty(\"pic_num\")]\n        public long? PicNum { get; set; }\n\n        [JsonProperty(\"mlevel\")]\n        public long? Mlevel { get; set; }\n\n        [JsonProperty(\"region_name\")]\n        public string? RegionName { get; set; }\n\n        [JsonProperty(\"region_opt\")]\n        public long? RegionOpt { get; set; }\n\n        [JsonProperty(\"analysis_extra\")]\n        public string? AnalysisExtra { get; set; }\n\n        [JsonProperty(\"mblog_menu_new_style\")]\n        public long? MblogMenuNewStyle { get; set; }\n\n        [JsonProperty(\"edit_config\")]\n        public EditConfig? EditConfig { get; set; }\n\n        [JsonProperty(\"page_info\")]\n        public PageInfo? PageInfo { get; set; }\n\n        [JsonProperty(\"pics\")]\n        //public List<Pic>? Pics { get; set; }\n        public object? Pics { get; set; }\n\n        [JsonProperty(\"live_photo\")]\n        public List<string>? LivePhoto { get; set; }\n\n        [JsonProperty(\"bid\")]\n        public string? Bid { get; set; }\n\n        [JsonProperty(\"safe_tags\")]\n        public long? SafeTags { get; set; }\n    }\n\n    public partial class RetweetedStatus\n    {\n    }\n\n    public partial class CommentManageInfo\n    {\n        [JsonProperty(\"comment_permission_type\")]\n        public long? CommentPermissionType { get; set; }\n\n        [JsonProperty(\"approval_comment_type\")]\n        public long? ApprovalCommentType { get; set; }\n\n        [JsonProperty(\"comment_sort_type\")]\n        public long? CommentSortType { get; set; }\n    }\n\n    public partial class DarwinTag\n    {\n        [JsonProperty(\"object_type\")]\n        public string? ObjectType { get; set; }\n\n        [JsonProperty(\"object_id\")]\n        public string? ObjectId { get; set; }\n\n        [JsonProperty(\"display_name\")]\n        public string? DisplayName { get; set; }\n\n        [JsonProperty(\"enterprise_uid\")]\n        public string? EnterpriseUid { get; set; }\n\n        [JsonProperty(\"bd_object_type\")]\n        public string? BdObjectType { get; set; }\n    }\n\n    public partial class EditConfig\n    {\n        [JsonProperty(\"edited\")]\n        public bool? Edited { get; set; }\n    }\n\n    public partial class NumberDisplayStrategy\n    {\n        [JsonProperty(\"apply_scenario_flag\")]\n        public long? ApplyScenarioFlag { get; set; }\n\n        [JsonProperty(\"display_text_min_number\")]\n        public long? DisplayTextMinNumber { get; set; }\n\n        [JsonProperty(\"display_text\")]\n        public string? DisplayText { get; set; }\n    }\n\n    public partial class PageInfo\n    {\n        [JsonProperty(\"type\")]\n        public string? Type { get; set; }\n\n        [JsonProperty(\"icon\")]\n        public string? Icon { get; set; }\n\n        [JsonProperty(\"page_pic\")]\n        public PagePic? PagePic { get; set; }\n\n        [JsonProperty(\"page_url\")]\n        public string? PageUrl { get; set; }\n\n        [JsonProperty(\"page_title\")]\n        public string? PageTitle { get; set; }\n\n        [JsonProperty(\"content1\")]\n        public string? Content1 { get; set; }\n\n        [JsonProperty(\"content2\")]\n        public string? Content2 { get; set; }\n\n        [JsonProperty(\"video_orientation\")]\n        public string? VideoOrientation { get; set; }\n\n        [JsonProperty(\"play_count\")]\n        public string? PlayCount { get; set; }\n\n        [JsonProperty(\"media_info\")]\n        public Mediainfo? Mediainfo { get; set; }\n\n        [JsonProperty(\"urls\")]\n        public Urls? Urls { get; set; }\n    }\n\n    public partial class Mediainfo\n    {\n        [JsonProperty(\"stream_url\")]\n        public string? StreamUrl { get; set; }\n\n        [JsonProperty(\"stream_url_hd\")]\n        public string? StreamUrlHd { get; set; }\n\n        [JsonProperty(\"duration\")]\n        public string? Duration { get; set; }\n    }\n\n    public partial class Urls\n    {\n        [JsonProperty(\"mp4_8k_mp4\")]\n        public string? Mp48kMp4 { get; set; }\n\n        [JsonProperty(\"mp4_4k_mp4\")]\n        public string? Mp44kMp4 { get; set; }\n\n        [JsonProperty(\"mp4_2k_mp4\")]\n        public string? Mp42kMp4 { get; set; }\n\n        [JsonProperty(\"mp4_1080p_mp4\")]\n        public string? Mp41080pMp4 { get; set; }\n\n        [JsonProperty(\"mp4_720p_mp4\")]\n        public string? Mp4720pMp4 { get; set; }\n\n        [JsonProperty(\"mp4_hd_mp4\")]\n        public string? Mp4HDMp4 { get; set; }\n\n        [JsonProperty(\"mp4_ld_mp4\")]\n        public string? Mp4LDMp4 { get; set; }\n    }\n\n    public partial class PagePic\n    {\n        [JsonProperty(\"url\")]\n        public string? Url { get; set; }\n\n        [JsonProperty(\"width\")]\n        public int? Width { get; set; }\n\n        [JsonProperty(\"height\")]\n        public int? Height { get; set; }\n    }\n\n    public partial class Pic\n    {\n        [JsonProperty(\"pid\")]\n        public string? Pid { get; set; }\n\n        [JsonProperty(\"url\")]\n        public string? Url { get; set; }\n\n        [JsonProperty(\"size\")]\n        public string? Size { get; set; }\n\n        [JsonProperty(\"geo\")]\n        public object? Geo { get; set; }\n\n        [JsonProperty(\"large\")]\n        public Large? Large { get; set; }\n\n        [JsonProperty(\"videoSrc\")]\n        public string? VideoSrc { get; set; }\n\n        [JsonProperty(\"type\")]\n        public string? Type { get; set; }\n    }\n\n    public partial class PicGeo\n    {\n        [JsonProperty(\"width\")]\n        public long? Width { get; set; }\n\n        [JsonProperty(\"height\")]\n        public long? Height { get; set; }\n\n        [JsonProperty(\"croped\")]\n        public bool? Croped { get; set; }\n    }\n\n    public partial class Large\n    {\n        [JsonProperty(\"size\")]\n        public string? Size { get; set; }\n\n        [JsonProperty(\"url\")]\n        public string? Url { get; set; }\n\n        [JsonProperty(\"geo\")]\n        public object? Geo { get; set; }\n    }\n\n    public partial class LargeGeo\n    {\n        [JsonProperty(\"width\")]\n        public long? Width { get; set; }\n\n        [JsonProperty(\"height\")]\n        public long? Height { get; set; }\n\n        [JsonProperty(\"croped\")]\n        public bool? Croped { get; set; }\n    }\n\n    public partial class User\n    {\n        [JsonProperty(\"id\")]\n        public long? Id { get; set; }\n\n        [JsonProperty(\"screen_name\")]\n        public string? ScreenName { get; set; }\n\n        [JsonProperty(\"profile_image_url\")]\n        public string? ProfileImageUrl { get; set; }\n\n        [JsonProperty(\"profile_url\")]\n        public string? ProfileUrl { get; set; }\n\n        [JsonProperty(\"close_blue_v\")]\n        public bool? CloseBlueV { get; set; }\n\n        [JsonProperty(\"description\")]\n        public string? Description { get; set; }\n\n        [JsonProperty(\"follow_me\")]\n        public bool? FollowMe { get; set; }\n\n        [JsonProperty(\"following\")]\n        public bool? Following { get; set; }\n\n        [JsonProperty(\"follow_count\")]\n        public long? FollowCount { get; set; }\n\n        [JsonProperty(\"followers_count\")]\n        public string? FollowersCount { get; set; }\n\n        [JsonProperty(\"cover_image_phone\")]\n        public string? CoverImagePhone { get; set; }\n\n        [JsonProperty(\"avatar_hd\")]\n        public string? AvatarHd { get; set; }\n\n        [JsonProperty(\"badge\")]\n        public Dictionary<string, long?>? Badge { get; set; }\n\n        [JsonProperty(\"statuses_count\")]\n        public long? StatusesCount { get; set; }\n\n        [JsonProperty(\"verified\")]\n        public bool? Verified { get; set; }\n\n        [JsonProperty(\"verified_type\")]\n        public long? VerifiedType { get; set; }\n\n        [JsonProperty(\"gender\")]\n        public string? Gender { get; set; }\n\n        [JsonProperty(\"mbtype\")]\n        public long? Mbtype { get; set; }\n\n        [JsonProperty(\"svip\")]\n        public long? Svip { get; set; }\n\n        [JsonProperty(\"urank\")]\n        public long? Urank { get; set; }\n\n        [JsonProperty(\"mbrank\")]\n        public long? Mbrank { get; set; }\n\n        [JsonProperty(\"followers_count_str\")]\n        public string? FollowersCountStr { get; set; }\n\n        [JsonProperty(\"verified_reason\")]\n        public string? VerifiedReason { get; set; }\n\n        [JsonProperty(\"like\")]\n        public bool? Like { get; set; }\n\n        [JsonProperty(\"like_me\")]\n        public bool? LikeMe { get; set; }\n\n        [JsonProperty(\"special_follow\")]\n        public bool? SpecialFollow { get; set; }\n    }\n\n    public partial class Visible\n    {\n        [JsonProperty(\"type\")]\n        public long? Type { get; set; }\n\n        [JsonProperty(\"list_id\")]\n        public long? ListId { get; set; }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/SettingsWindow.xaml",
    "content": "﻿<mica:MicaWindow\n  x:Class=\"WeiboAlbumDownloader.SettingsWindow\"\n  xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n  xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n  xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n  xmlns:local=\"clr-namespace:WeiboAlbumDownloader\"\n  xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n  xmlns:mica=\"clr-namespace:MicaWPF.Controls;assembly=MicaWPF\"\n  Title=\"设置\"\n  Width=\"1200\"\n  Height=\"567\"\n  FontFamily=\"微软雅黑\"\n  Icon=\"/weibo.ico\"\n  ResizeMode=\"NoResize\"\n  SystemBackdropType=\"Acrylic\"\n  TitleBarType=\"WinUI\"\n  WindowStartupLocation=\"CenterOwner\"\n  mc:Ignorable=\"d\">\n  <ScrollViewer HorizontalScrollBarVisibility=\"Disabled\">\n    <Grid>\n      <StackPanel Margin=\"8\">\n        <TextBlock\n          Margin=\"8,0,0,0\"\n          VerticalAlignment=\"Center\"\n          Foreground=\"Gray\"\n          Text=\"数据源\" />\n        <ComboBox\n          x:Name=\"ComboBox_DataSource\"\n          Margin=\"16,8,0,8\"\n          ToolTip=\"不懂的话选择m.weibo.cn\" />\n\n        <TextBlock\n          Margin=\"8,8,0,0\"\n          VerticalAlignment=\"Center\"\n          Foreground=\"Gray\"\n          Text=\"Cookie\" />\n        <Grid Margin=\"8,8,0,0\">\n          <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition />\n          </Grid.ColumnDefinitions>\n          <mica:Button\n            Width=\"200\"\n            Height=\"32\"\n            Margin=\"8,0\"\n            HorizontalAlignment=\"Left\"\n            Background=\"LightYellow\"\n            Click=\"GetCookie\"\n            Tag=\"cn\"\n            ToolTip=\"打开手机版微博，扫描二维码确认登陆\">\n            <StackPanel Orientation=\"Horizontal\">\n              <Image Width=\"18\" Source=\"/Assets/cookie.png\" />\n              <TextBlock\n                Margin=\"8,0,0,0\"\n                VerticalAlignment=\"Center\"\n                Text=\"扫码获取weibo.cn Cookie\" />\n            </StackPanel>\n          </mica:Button>\n          <mica:TextBox\n            x:Name=\"TextBox_WeiboCnCookie\"\n            Grid.Column=\"1\"\n            Height=\"32\"\n            Margin=\"8,0,0,0\"\n            Watermark=\"weibo.cn cookie\" />\n        </Grid>\n\n        <Grid Margin=\"8,8,0,0\">\n          <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition />\n          </Grid.ColumnDefinitions>\n          <mica:Button\n            Width=\"200\"\n            Height=\"32\"\n            Margin=\"8,0\"\n            HorizontalAlignment=\"Left\"\n            Background=\"LightYellow\"\n            Click=\"GetCookie\"\n            Tag=\"com\"\n            ToolTip=\"打开手机版微博，扫描二维码确认登陆\">\n            <StackPanel Orientation=\"Horizontal\">\n              <Image Width=\"18\" Source=\"/Assets/cookie.png\" />\n              <TextBlock\n                Margin=\"8,0,0,0\"\n                VerticalAlignment=\"Center\"\n                Text=\"扫码获取weibo.com Cookie\" />\n            </StackPanel>\n          </mica:Button>\n          <mica:TextBox\n            x:Name=\"TextBox_WeiboComCookie\"\n            Grid.Column=\"1\"\n            Height=\"32\"\n            Margin=\"8,0,0,0\"\n            Watermark=\"weibo.com cookie\" />\n        </Grid>\n\n        <TextBlock\n          Grid.Row=\"1\"\n          Margin=\"16,8,8,0\"\n          VerticalAlignment=\"Center\"\n          Foreground=\"Gray\"\n          TextWrapping=\"Wrap\">\n          <Run Text=\"扫码无法打开浏览器时，可自行在PC浏览器打开\" />\n          <Hyperlink NavigateUri=\"https://weibo.com/\" RequestNavigate=\"Hyperlink_RequestNavigate\">weibo.com</Hyperlink>\n          <Run Text=\"，点击某一用户头像，进入主页。uid就是地址栏中的最后一串数字，比如https://weibo.com/u/1000000000。Cookie可以通过点击上方按钮打开页面扫码获取，或者按F12进入控制台，网络-全部，在名称栏选择uid，标头-请求标头-Cookie。\" />\n          <Hyperlink NavigateUri=\"https://weibo.com/\" RequestNavigate=\"Hyperlink_RequestNavigate\">weibo.com</Hyperlink>\n          <Run Text=\"和\" />\n          <Hyperlink NavigateUri=\"https://weibo.cn\" RequestNavigate=\"Hyperlink_RequestNavigate\">weibo.cn</Hyperlink>\n          <Run Text=\"cookie不一样，请注意区分。\" />\n        </TextBlock>\n\n        <TextBlock\n          Margin=\"8,16,0,0\"\n          VerticalAlignment=\"Center\"\n          Foreground=\"Gray\"\n          Text=\"消息推送\" />\n        <Grid Margin=\"8,8,0,0\">\n          <Grid.ColumnDefinitions>\n            <ColumnDefinition Width=\"Auto\" />\n            <ColumnDefinition />\n          </Grid.ColumnDefinitions>\n          <mica:Button\n            Width=\"200\"\n            Height=\"32\"\n            Margin=\"8,0\"\n            HorizontalAlignment=\"Left\"\n            Background=\"LightYellow\"\n            Click=\"OpenPushPlus\"\n            Tag=\"com\"\n            ToolTip=\"打开PushPlus官网获取\">\n            <StackPanel Orientation=\"Horizontal\">\n              <Image Width=\"18\" Source=\"/Assets/pushplus.png\" />\n              <TextBlock\n                Margin=\"8,0,0,0\"\n                VerticalAlignment=\"Center\"\n                Text=\"打开PushPlus官网获取        \" />\n            </StackPanel>\n          </mica:Button>\n          <mica:TextBox\n            x:Name=\"TextBox_PushPlusToken\"\n            Grid.Column=\"1\"\n            Width=\"554\"\n            Height=\"32\"\n            Margin=\"8,0,0,0\"\n            HorizontalAlignment=\"Left\"\n            Watermark=\"Push plus token\" />\n        </Grid>\n\n        <TextBlock\n          Margin=\"8,16,0,0\"\n          VerticalAlignment=\"Center\"\n          Foreground=\"Gray\"\n          Text=\"下载设置\" />\n        <UniformGrid Margin=\"16,8,0,0\" Columns=\"3\">\n          <mica:ToggleSwitch\n            x:Name=\"ToggleSwitch_ShowHeadImage\"\n            Content=\"显示作者头像\"\n            Foreground=\"Gray\" />\n          <mica:ToggleSwitch\n            x:Name=\"ToggleSwitch_EnableDownloadVideo\"\n            Content=\"下载视频\"\n            Foreground=\"Gray\" />\n          <mica:ToggleSwitch\n            x:Name=\"ToggleSwitch_EnableDownloadLivePhoto\"\n            Content=\"下载Live Photo\"\n            Foreground=\"Gray\" />\n\n          <StackPanel Orientation=\"Horizontal\">\n            <mica:ToggleSwitch\n              x:Name=\"ToggleSwitch_DatetimeRange\"\n              Margin=\"0,8,0,0\"\n              Checked=\"ToggleSwitch_DatetimeRangeChecked\"\n              Content=\"启用时间范围\"\n              Foreground=\"Gray\"\n              ToolTip=\"开启后仅下载指定时间范围内的微博数据\"\n              Unchecked=\"ToggleSwitch_DatetimeRangeUnchecked\" />\n            <TextBlock\n              Margin=\"16,14,0,0\"\n              FontSize=\"13\"\n              Foreground=\"Gray\"\n              Text=\"自\" />\n            <DatePicker\n              x:Name=\"DatePicker_Start\"\n              Width=\"100\"\n              Margin=\"2,12,0,0\"\n              BorderThickness=\"0\"\n              Foreground=\"Gray\" />\n            <TextBlock\n              Margin=\"2,13,0,0\"\n              FontSize=\"13\"\n              Foreground=\"Gray\"\n              Text=\"至最新日期\" />\n          </StackPanel>\n\n          <StackPanel Orientation=\"Horizontal\">\n            <mica:ToggleSwitch\n              x:Name=\"ToggleSwitch_Crontab\"\n              Margin=\"0,8,0,0\"\n              Checked=\"ToggleSwitch_Checked\"\n              Content=\"启用定时下载\"\n              Foreground=\"Gray\"\n              ToolTip=\"定时任务会在设置的Crontab时间启动，自动循环uidList.txt中所有用户，执行下载任务\"\n              Unchecked=\"ToggleSwitch_Unchecked\" />\n            <mica:TextBox\n              x:Name=\"TextBox_Crontab\"\n              Width=\"160\"\n              Height=\"32\"\n              Margin=\"16,8,0,0\"\n              TextWrapping=\"Wrap\"\n              ToolTip=\"例如，14 2 * * *，代表每天凌晨2点14分开始下载\" />\n          </StackPanel>\n\n          <mica:ToggleSwitch\n            x:Name=\"ToggleSwitch_EnableShortenName\"\n            Margin=\"0,8,0,0\"\n            Content=\"精简图片命名\"\n            Foreground=\"Gray\"\n            ToolTip=\"启用后图片仅以日期+编号命名，文件名中不在包含博文内容\" />\n\n          <StackPanel Orientation=\"Horizontal\">\n            <TextBlock\n              Margin=\"0,8,0,0\"\n              VerticalAlignment=\"Center\"\n              Foreground=\"Gray\"\n              Text=\"自动跳到下一个用户下载的计数\" />\n            <mica:TextBox\n              x:Name=\"TextBox_SkipCount\"\n              Width=\"126\"\n              Height=\"32\"\n              Margin=\"8,8,0,0\"\n              TextWrapping=\"Wrap\"\n              ToolTip=\"下载用户的文件如果在本地已经存在设置的个数，软件就认为当前用户已经下载完成\" />\n          </StackPanel>\n        </UniformGrid>\n\n        <StackPanel\n          Margin=\"0,16,0,0\"\n          HorizontalAlignment=\"Center\"\n          Orientation=\"Horizontal\">\n          <mica:Button\n            Width=\"100\"\n            Height=\"32\"\n            Background=\"#FFBF00\"\n            Click=\"Confirm_Click\"\n            Content=\"OK\" />\n          <mica:Button\n            Width=\"100\"\n            Height=\"32\"\n            Margin=\"24,0,0,0\"\n            HorizontalAlignment=\"Right\"\n            VerticalAlignment=\"Top\"\n            Click=\"OpenGithub\"\n            ToolTip=\"Fork me on Github\">\n            <StackPanel Orientation=\"Horizontal\">\n              <Image Width=\"20\" Source=\"/Assets/github.png\" />\n            </StackPanel>\n          </mica:Button>\n        </StackPanel>\n      </StackPanel>\n    </Grid>\n  </ScrollViewer>\n</mica:MicaWindow>\n"
  },
  {
    "path": "WeiboAlbumDownloader/SettingsWindow.xaml.cs",
    "content": "﻿using MicaWPF.Controls;\nusing Newtonsoft.Json;\nusing System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Windows;\nusing System.Windows.Controls;\nusing WeiboAlbumDownloader.Enums;\nusing WeiboAlbumDownloader.Helpers;\nusing WeiboAlbumDownloader.Models;\n\nnamespace WeiboAlbumDownloader\n{\n    /// <summary>\n    /// SettingsWindow.xaml 的交互逻辑\n    /// </summary>\n    public partial class SettingsWindow : MicaWindow\n    {\n        public SettingsWindow()\n        {\n            InitializeComponent();\n            this.Loaded += SettingsWindow_Loaded;\n        }\n\n        private void SettingsWindow_Loaded(object sender, RoutedEventArgs e)\n        {\n            ComboBox_DataSource.ItemsSource = new List<string>()\n            {\n                \"m.weibo.cn(获取用户的时间流，推荐使用!!!)\",\n                \"weibo.cn    (获取用户的时间流，不过只能获取原创微博相册)\",\n                \"weibo.com (获取相册信息，可以获取原创微博相册、头像相册、自拍相册等。少数用户存在获取失败的问题，怀疑是微博内部api不统一造成的。截止日期仅对微博配图生效)\",\n                \"weibo.com (获取用户的ajax相册，可以获取原创微博相册、头像相册、自拍相册等。但是获取不到博文信息，所以无法重命名图片和修改图片日期。貌似还获取不全。截止日期不生效，不推荐使用！！！)\",\n            };\n\n            if (File.Exists(\"Settings.json\"))\n            {\n                string settingsContent = File.ReadAllText(\"Settings.json\");\n                var settings = JsonConvert.DeserializeObject<SettingsModel>(settingsContent);\n                if (settings?.DataSource == WeiboDataSource.WeiboCnMobile) ComboBox_DataSource.SelectedIndex = 0;\n                else if (settings?.DataSource == WeiboDataSource.WeiboCn) ComboBox_DataSource.SelectedIndex = 1;\n                else if (settings?.DataSource == WeiboDataSource.WeiboCom1) ComboBox_DataSource.SelectedIndex = 2;\n                else if (settings?.DataSource == WeiboDataSource.WeiboCom2) ComboBox_DataSource.SelectedIndex = 3;\n\n                TextBox_WeiboCnCookie.Text = settings?.WeiboCnCookie;\n                TextBox_WeiboComCookie.Text = settings?.WeiboComCookie;\n                TextBox_PushPlusToken.Text = settings?.PushPlusToken;\n                ToggleSwitch_ShowHeadImage.IsChecked = settings?.ShowHeadImage;\n                ToggleSwitch_EnableDownloadVideo.IsChecked = settings?.EnableDownloadVideo;\n                ToggleSwitch_EnableDownloadLivePhoto.IsChecked = settings?.EnableDownloadLivePhoto;\n                ToggleSwitch_EnableShortenName.IsChecked = settings?.EnableShortenName;\n                ToggleSwitch_Crontab.IsChecked = settings?.EnableCrontab;\n                TextBox_Crontab.Text = settings?.Crontab;\n                TextBox_SkipCount.Text = settings?.CountDownloadedSkipToNextUser.ToString();\n                ToggleSwitch_DatetimeRange.IsChecked = settings?.EnableDatetimeRange;\n                DatePicker_Start.SelectedDate = settings?.StartDateTime;\n            }\n        }\n\n        private void GetCookie(object sender, RoutedEventArgs e)\n        {\n            var tag = (sender as MicaWPF.Controls.Button)?.Tag as string;\n            if (tag == \"cn\")\n            {\n                TextBox_WeiboCnCookie.Text = SeleniumHelper.GetCookie(Enums.WeiboDataSource.WeiboCnMobile);\n            }\n            else if (tag == \"com\")\n            {\n                TextBox_WeiboComCookie.Text = SeleniumHelper.GetCookie(Enums.WeiboDataSource.WeiboCom1);\n            }\n        }\n\n        private void Confirm_Click(object sender, RoutedEventArgs e)\n        {\n            //先判断日期是否合法\n            if (ToggleSwitch_DatetimeRange.IsChecked == true)\n            {\n                if (DatePicker_Start.SelectedDate == null)\n                {\n                    MessageBox.Show(\"启用时间范围后，必须要设置起始日期。\");\n                    return;\n                }\n                else\n                {\n                    if (DatePicker_Start.SelectedDate > DateTime.Now)\n                    {\n                        MessageBox.Show(\"启用时间范围后，起始日期不能是未来的时间。\");\n                        return;\n                    }\n                }\n            }\n\n            var ds = WeiboDataSource.WeiboCnMobile;\n            if (ComboBox_DataSource.SelectedIndex == 0)\n            {\n                ds = WeiboDataSource.WeiboCnMobile;\n            }\n            else if (ComboBox_DataSource.SelectedIndex == 1)\n            {\n                ds = WeiboDataSource.WeiboCn;\n            }\n            else if (ComboBox_DataSource.SelectedIndex == 2)\n            {\n                ds = WeiboDataSource.WeiboCom1;\n            }\n            else if (ComboBox_DataSource.SelectedIndex == 3)\n            {\n                ds = WeiboDataSource.WeiboCom2;\n            }\n\n            //再写入Json\n            var settings = new SettingsModel()\n            {\n                DataSource = ds,\n                WeiboCnCookie = TextBox_WeiboCnCookie.Text,\n                WeiboComCookie = TextBox_WeiboComCookie.Text,\n                PushPlusToken = TextBox_PushPlusToken.Text,\n                ShowHeadImage = (bool)ToggleSwitch_ShowHeadImage.IsChecked!,\n                EnableDownloadVideo = (bool)ToggleSwitch_EnableDownloadVideo.IsChecked!,\n                EnableDownloadLivePhoto = (bool)ToggleSwitch_EnableDownloadLivePhoto.IsChecked!,\n                EnableShortenName = (bool)ToggleSwitch_EnableShortenName.IsChecked!,\n                EnableCrontab = (bool)ToggleSwitch_Crontab.IsChecked!,\n                Crontab = TextBox_Crontab.Text,\n                CountDownloadedSkipToNextUser = string.IsNullOrEmpty(TextBox_SkipCount.Text) ? 20 : Convert.ToInt32(TextBox_SkipCount.Text),\n                EnableDatetimeRange = (bool)ToggleSwitch_DatetimeRange.IsChecked!,\n                StartDateTime = DatePicker_Start.SelectedDate,\n            };\n            File.WriteAllText(\"Settings.json\", JsonConvert.SerializeObject(settings, Formatting.Indented));\n            this.Close();\n        }\n\n        private void ToggleSwitch_Checked(object sender, RoutedEventArgs e)\n        {\n            TextBox_Crontab.IsEnabled = true;\n        }\n\n        private void ToggleSwitch_Unchecked(object sender, RoutedEventArgs e)\n        {\n            TextBox_Crontab.IsEnabled = false;\n        }\n\n        private void OpenPushPlus(object sender, RoutedEventArgs e)\n        {\n            Process.Start(new ProcessStartInfo(\"https://www.pushplus.plus/uc.html\") { UseShellExecute = true });\n        }\n\n        private void ToggleSwitch_DatetimeRangeChecked(object sender, RoutedEventArgs e)\n        {\n            DatePicker_Start.IsEnabled = true;\n        }\n\n        private void ToggleSwitch_DatetimeRangeUnchecked(object sender, RoutedEventArgs e)\n        {\n            DatePicker_Start.IsEnabled = false;\n        }\n\n        private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)\n        {\n            Process.Start(\"explorer.exe\", e.Uri.AbsoluteUri);\n        }\n\n        private void OpenGithub(object sender, RoutedEventArgs e)\n        {\n            Process.Start(new ProcessStartInfo(\"https://github.com/hupo376787/WeiboAlbumDownloader\") { UseShellExecute = true });\n        }\n    }\n}\n"
  },
  {
    "path": "WeiboAlbumDownloader/WeiboAlbumDownloader.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net6.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <UseWPF>true</UseWPF>\n    <ApplicationIcon>weibo.ico</ApplicationIcon>\n    <SatelliteResourceLanguages>zh-Hans</SatelliteResourceLanguages>\n    <Company>Caiwei Studio</Company>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <None Remove=\"Assets\\batch.png\" />\n    <None Remove=\"Assets\\cloud-computing.png\" />\n    <None Remove=\"Assets\\cookie.png\" />\n    <None Remove=\"Assets\\download.png\" />\n    <None Remove=\"Assets\\github.png\" />\n    <None Remove=\"Assets\\pushplus.png\" />\n    <None Remove=\"Assets\\settings.png\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Assets\\batch.png\" />\n    <Resource Include=\"Assets\\github.png\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Resource Include=\"Assets\\cloud-computing.png\" />\n    <Resource Include=\"Assets\\cookie.png\" />\n    <Resource Include=\"Assets\\download.png\" />\n    <Resource Include=\"Assets\\pushplus.png\" />\n    <Resource Include=\"Assets\\settings.png\" />\n    <Resource Include=\"weibo.ico\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"CronExpressionDescriptor\" Version=\"2.44.0\" />\n    <PackageReference Include=\"HtmlAgilityPack\" Version=\"1.12.4\" />\n    <PackageReference Include=\"MicaWPF\" Version=\"6.3.0\" />\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"13.0.4\" />\n    <PackageReference Include=\"Selenium.WebDriver\" Version=\"4.39.0\" />\n    <PackageReference Include=\"Selenium.WebDriver.ChromeDriver\" Version=\"143.0.7499.4000\" />\n    <PackageReference Include=\"Sentry\" Version=\"5.16.2\" />\n    <PackageReference Include=\"TimeCrontab\" Version=\"3.7.0\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "WeiboAlbumDownloader.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.7.34221.43\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"WeiboAlbumDownloader\", \"WeiboAlbumDownloader\\WeiboAlbumDownloader.csproj\", \"{3A020274-F6B7-459D-9FB7-2AF6AFF680B9}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{3A020274-F6B7-459D-9FB7-2AF6AFF680B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{3A020274-F6B7-459D-9FB7-2AF6AFF680B9}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{3A020274-F6B7-459D-9FB7-2AF6AFF680B9}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{3A020274-F6B7-459D-9FB7-2AF6AFF680B9}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {7CBDAC70-9D3A-4CF9-9DF3-BA04FD4C2B74}\n\tEndGlobalSection\nEndGlobal\n"
  }
]