加入專案檔案。

This commit is contained in:
2024-10-01 09:59:42 +08:00
parent 3d05913418
commit 6e145daf52
25 changed files with 1830 additions and 0 deletions

25
EHDownloader.sln Normal file
View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.88
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EHDownloader", "EHDownloader\EHDownloader.csproj", "{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}.Debug|Any CPU.Build.0 = Debug|Any CPU
{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}.Release|Any CPU.ActiveCfg = Release|Any CPU
{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FCA04208-1958-4F13-82E2-1904DB013042}
EndGlobalSection
EndGlobal

24
EHDownloader/App.config Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="EHDownloader.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<applicationSettings>
<EHDownloader.Properties.Settings>
<setting name="BaseFolder" serializeAs="String">
<value>D:\books</value>
</setting>
<setting name="ipb_member_id" serializeAs="String">
<value>1512454</value>
</setting>
<setting name="ipb_pass_hash" serializeAs="String">
<value>6b31380b7ce97c6d455772ea3ead014f</value>
</setting>
</EHDownloader.Properties.Settings>
</applicationSettings>
</configuration>

303
EHDownloader/Book.cs Normal file
View File

@@ -0,0 +1,303 @@
using EHDownloader.FileContainer;
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader
{
enum FetchBookResults
{
LoadPageFailed,
FetchTitleFailed,
FetchPageSetFailed,
FetchPageLinkFailed,
Completed,
}
enum DownloadBookResults
{
FetchPageSetFailed,
FetchPageLinkFailed,
Completed,
DownloadImageFailed,
}
enum BookStates
{
[Description("等待")]
Wait,
//[Description("擷取頁面失敗")]
//FetchError,
[Description("載入首頁失敗")]
LoadFailed,
[Description("擷取標題失敗")]
FetchTitleFailed,
[Description("擷取頁集合失敗")]
FetchPageSetFailed,
[Description("擷取頁連結失敗")]
FetchPageLinkFailed,
[Description("擷取頁面完成")]
FetchCompleted,
[Description("下載完成")]
DownloadCompleted,
[Description("下載完成, 但是有些頁面出錯")]
DownloadFinishedButSomePageError,
Compressed,
}
class Book
{
private const string PageSetXPath = @"//table[@class='ptb']//a";
private const string SubTitleXPath = @"//*[@id='gj']";
private const string TitleXPath = @"//*[@id='gn']";
private const string PageLinksXPath = @"//*[@id='gdt']//a";
static IFileContainProvider fileContainerProvider = new FolderFileContainerProvider();
public string ParentFolder { get; private set; }
public string Url { get; private set; }
public string Title { get; private set; }
public DateTime DownloadDateTime { get; private set; }
public List<Page> Pages { get; private set; }
public int PageCount => Pages.Count;
public BookStates BookState { get; set; } = BookStates.Wait;
public Book(string url, string parentFolder)
{
if (!url.EndsWith("/"))
url += '/';
Url = url;
ParentFolder = parentFolder;
}
IHtmlDocumentProvider _htmlDocProvider = new HtmlDocument_HttpClient();
public async Task<FetchBookResults> FetchAsync()
{
// SortedSet<string> pageSets = new SortedSet<string>();
SortedDictionary<int, string> pageSets = new SortedDictionary<int, string>();
var cookieContainer = WebHelper.GetCookieContainer();
Uri uri = new Uri(Url);
cookieContainer.Add(new Cookie("nw", "1", uri.PathAndQuery, uri.Host));
HtmlDocument doc;
try
{
doc = await _htmlDocProvider.GetDocumentAsync(Url);
}
catch (Exception ex)
{
BookState = BookStates.LoadFailed;
return FetchBookResults.LoadPageFailed;
}
try
{
Title = parseTitle(doc);
}
catch
{
BookState = BookStates.FetchTitleFailed;
return FetchBookResults.FetchTitleFailed;
}
try
{
// get pagesets
var htmlPageSets = doc.DocumentNode.SelectNodes(PageSetXPath);
if (htmlPageSets == null)
{
BookState = BookStates.FetchPageSetFailed;
return FetchBookResults.FetchPageSetFailed;
}
foreach (var htmlPageSet in htmlPageSets)
{
var pageSetUrl = htmlPageSet.Attributes["href"].Value;
if (pageSetUrl == Url)
continue;
var querys = System.Web.HttpUtility.ParseQueryString(new Uri(pageSetUrl).Query);
int pageIndex = int.Parse(querys["p"]);
if (!pageSets.ContainsKey(pageIndex))
pageSets.Add(pageIndex, pageSetUrl);
}
//System.Web.HttpUtility.
}
catch
{
BookState = BookStates.FetchPageSetFailed;
return FetchBookResults.FetchTitleFailed;
}
// add other page sets
if (pageSets.Count > 0)
{
int lastPageIndex = pageSets.Last().Key;
for (int i = 1; i < lastPageIndex; i++)
{
if (!pageSets.ContainsKey(i))
{
pageSets.Add(i, $"{Url}?p={i}");
}
}
}
try
{
Pages = new List<Page>(FetchPages(doc));
foreach (var pageSetUrl in pageSets.Values)
{
Pages.AddRange(await FetchPages(pageSetUrl));
}
}
catch
{
BookState = BookStates.FetchPageLinkFailed;
return FetchBookResults.FetchPageLinkFailed;
}
for (int i = 0; i < Pages.Count; i++)
{
Pages[i].PageNumber = i + 1;
}
BookState = BookStates.FetchCompleted;
return FetchBookResults.Completed;
}
private string parseTitle(HtmlDocument doc)
{
var bookTitleNode = doc.DocumentNode.SelectNodes(SubTitleXPath).FirstOrDefault();
string title = bookTitleNode.InnerHtml;
if (!string.IsNullOrWhiteSpace(title))
return WebUtility.HtmlDecode(title);
bookTitleNode = doc.DocumentNode.SelectNodes(TitleXPath).FirstOrDefault();
title = bookTitleNode.InnerHtml;
return WebUtility.HtmlDecode(title);
}
public async Task DownloadBookAsync(IProgress<DownloadProgressInfo> progress = null)
{
var fileContainer = fileContainerProvider.CreateNewContainer(getFolderPath());
DownloadDateTime = DateTime.Now;
int tryTimes = 0;
int tryLimit = 3;
while (tryTimes++ < tryLimit)
{
foreach (var page in Pages.Where(p => p.Result != DownloadPageResults.Completed))
{
try
{
progress?.Report(new DownloadProgressInfo() { Book = this, Page = page });
await page.DownloadToAsync(fileContainer);
}
catch (Exception)
{
}
}
if (Pages.All(p => p.Result == DownloadPageResults.Completed))
{
BookState = BookStates.DownloadCompleted;
return;
}
}
BookState = BookStates.DownloadFinishedButSomePageError;
}
private string GetFolderName()
{
string folderName = Title;
foreach (var ch in System.IO.Path.GetInvalidFileNameChars())
folderName = folderName.Replace(ch, ' ');
return folderName;
}
private async Task<IEnumerable<Page>> FetchPages(string url)
{
return FetchPages(await _htmlDocProvider.GetDocumentAsync(url));
}
private static IEnumerable<Page> FetchPages(HtmlDocument doc)
{
var htmlLinks = doc.DocumentNode.SelectNodes(PageLinksXPath);
foreach (var htmlLink in htmlLinks)
{
var linkStr = htmlLink.Attributes["href"].Value;
yield return new Page(linkStr);
}
}
public async Task CompressAsync()
{
string folderPath = getFolderPath();
if (BookState != BookStates.DownloadCompleted)
return;
if (!System.IO.Directory.Exists(ParentFolder))
return;
ZipFileContainer zipFileContainer = new ZipFileContainer(folderPath);
foreach (var file in System.IO.Directory.GetFiles(folderPath))
{
using (var fileStream = System.IO.File.OpenRead(file))
using (var outStream = zipFileContainer.CreateFile(System.IO.Path.GetFileName(file)))
await fileStream.CopyToAsync(outStream);
}
zipFileContainer.Close();
if (Settings.Instance.DeleteDownloadFolderAfterCompressed)
{
System.IO.Directory.Delete(folderPath, true);
}
BookState = BookStates.Compressed;
}
private string getFolderPath()
{
return System.IO.Path.Combine(ParentFolder, GetFolderName());
}
}
internal class DownloadProgressInfo
{
public Book Book { get; set; }
public Page Page { get; set; }
public int PageNumber => Page.PageNumber;
public int PageCount => Book.PageCount;
}
static public class EnumHelper
{
public static string GetDescription(this Enum source)
{
System.Reflection.FieldInfo fi = source.GetType().GetField(source.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute), false);
if (attributes.Length > 0) return attributes[0].Description;
else return source.ToString();
}
}
}

52
EHDownloader/DbContext.cs Normal file
View File

@@ -0,0 +1,52 @@
using Dapper;
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader
{
class DbContext
{
public DbContext()
{
if (File.Exists(Settings.GetDbPath())) return;
using (var cn = new SQLiteConnection(Settings.GetConnectString()))
{
cn.Execute(@"
CREATE TABLE Book (
Url VARCHAR(256),
Title VARNCHAR(256),
PageCount smallint,
DownloadDateTime datetime,
CONSTRAINT Book_PK PRIMARY KEY (Url)
)");
}
}
public bool PageIsExists(string url)
{
using (var cn = new SQLiteConnection(Settings.GetConnectString()))
{
var query =
"select 1 from Book where Url=(@url)";
return cn.Query(query, new { url }).FirstOrDefault() != null;
}
}
public void InsertPage(Book book)
{
using (var cn = new SQLiteConnection(Settings.GetConnectString()))
{
var insertScript =
"INSERT INTO Book VALUES (@Url,@Title,@PageCount,@DownloadDateTime)";
cn.Execute(insertScript, book);
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{824F3CE9-87F6-48E0-B9D6-D5F19DC33960}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>EHDownloader</RootNamespace>
<AssemblyName>EHDownloader</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dapper, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Dapper.2.0.4\lib\netstandard2.0\Dapper.dll</HintPath>
</Reference>
<Reference Include="HtmlAgilityPack, Version=1.11.12.0, Culture=neutral, PublicKeyToken=bd319b19eaf3b43a, processorArchitecture=MSIL">
<HintPath>..\packages\HtmlAgilityPack.1.11.12\lib\Net45\HtmlAgilityPack.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SQLite, Version=1.0.111.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139, processorArchitecture=MSIL">
<HintPath>..\packages\System.Data.SQLite.Core.1.0.111.0\lib\net46\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Web" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Book.cs" />
<Compile Include="DbContext.cs" />
<Compile Include="FileContainer\FileContainerBase.cs" />
<Compile Include="FileContainer\FolderFileContainer.cs" />
<Compile Include="FileContainer\FolderFileContainerProvider.cs" />
<Compile Include="FileContainer\IFileContainProvider.cs" />
<Compile Include="FileContainer\ZipFileContainer.cs" />
<Compile Include="FileContainer\ZipFileContainerProvider.cs" />
<Compile Include="Form1.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Form1.Designer.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
<Compile Include="HtmlDocumentProvider.cs" />
<Compile Include="HtmlWebHelper.cs" />
<Compile Include="Page.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Settings.cs" />
<EmbeddedResource Include="Form1.resx">
<DependentUpon>Form1.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<None Include="packages.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets" Condition="Exists('..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>此專案參考這部電腦上所缺少的 NuGet 套件。請啟用 NuGet 套件還原,以下載該套件。如需詳細資訊,請參閱 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的檔案是 {0}。</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\System.Data.SQLite.Core.1.0.111.0\build\net46\System.Data.SQLite.Core.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
abstract class FileContainerBase
{
public string Name { get; private set; }
public FileContainerBase(string name)
{
Name = name;
}
public abstract Stream CreateFile(string fileName);
public virtual void Close() { }
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
class FolderFileContainer : FileContainerBase
{
public FolderFileContainer(string name) : base(name)
{
System.IO.Directory.CreateDirectory(name);
}
public override Stream CreateFile(string fileName)
{
return File.Create(Path.Combine(Name, fileName));
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
class FolderFileContainerProvider : IFileContainProvider
{
public FileContainerBase CreateNewContainer(string name)
{
return new FolderFileContainer(name);
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
interface IFileContainProvider
{
FileContainerBase CreateNewContainer(string name);
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
class ZipFileContainer : FileContainerBase
{
private ZipArchive _zipArchive;
public ZipFileContainer(string name) : base(name)
{
if (!name.EndsWith(".zip")) name += ".zip";
_zipArchive = new ZipArchive(File.Create(name), ZipArchiveMode.Create, false, Encoding.UTF8);
}
public override Stream CreateFile(string fileName)
{
return _zipArchive
.CreateEntry(fileName, CompressionLevel.NoCompression)
.Open();
}
public override void Close()
{
_zipArchive.Dispose();
base.Close();
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader.FileContainer
{
class ZipFileContainerProvider : IFileContainProvider
{
public FileContainerBase CreateNewContainer(string name)
{
return new ZipFileContainer(name);
}
}
}

139
EHDownloader/Form1.Designer.cs generated Normal file
View File

@@ -0,0 +1,139 @@
namespace EHDownloader
{
partial class Form1
{
/// <summary>
/// 設計工具所需的變數。
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 清除任何使用中的資源。
/// </summary>
/// <param name="disposing">如果應該處置受控資源則為 true否則為 false。</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 此為設計工具支援所需的方法 - 請勿使用程式碼編輯器修改
/// 這個方法的內容。
/// </summary>
private void InitializeComponent()
{
this.btn_Add = new System.Windows.Forms.Button();
this.txt_url = new System.Windows.Forms.TextBox();
this.lv_Tasks = new System.Windows.Forms.ListView();
this.columnHeader1 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.columnHeader2 = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
this.btn_Retry = new System.Windows.Forms.Button();
this.txt_folder = new System.Windows.Forms.TextBox();
this.btn_resetFolder = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// btn_Add
//
this.btn_Add.Location = new System.Drawing.Point(713, 12);
this.btn_Add.Name = "btn_Add";
this.btn_Add.Size = new System.Drawing.Size(75, 23);
this.btn_Add.TabIndex = 0;
this.btn_Add.Text = "Add";
this.btn_Add.UseVisualStyleBackColor = true;
this.btn_Add.Click += new System.EventHandler(this.btn_Add_Click);
//
// txt_url
//
this.txt_url.Location = new System.Drawing.Point(13, 12);
this.txt_url.Name = "txt_url";
this.txt_url.Size = new System.Drawing.Size(694, 22);
this.txt_url.TabIndex = 1;
this.txt_url.Text = "https://exhentai.org/g/2621788/e4178a8e4b/";
//
// lv_Tasks
//
this.lv_Tasks.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] {
this.columnHeader1,
this.columnHeader2});
this.lv_Tasks.HideSelection = false;
this.lv_Tasks.Location = new System.Drawing.Point(13, 69);
this.lv_Tasks.Name = "lv_Tasks";
this.lv_Tasks.Size = new System.Drawing.Size(775, 369);
this.lv_Tasks.TabIndex = 2;
this.lv_Tasks.UseCompatibleStateImageBehavior = false;
this.lv_Tasks.View = System.Windows.Forms.View.Details;
//
// columnHeader1
//
this.columnHeader1.Text = "Url";
this.columnHeader1.Width = 600;
//
// columnHeader2
//
this.columnHeader2.Text = "State";
this.columnHeader2.Width = 108;
//
// btn_Retry
//
this.btn_Retry.Location = new System.Drawing.Point(13, 445);
this.btn_Retry.Name = "btn_Retry";
this.btn_Retry.Size = new System.Drawing.Size(75, 23);
this.btn_Retry.TabIndex = 3;
this.btn_Retry.Text = "Retry";
this.btn_Retry.UseVisualStyleBackColor = true;
this.btn_Retry.Click += new System.EventHandler(this.Btn_Retry_Click);
//
// txt_folder
//
this.txt_folder.Location = new System.Drawing.Point(13, 41);
this.txt_folder.Name = "txt_folder";
this.txt_folder.Size = new System.Drawing.Size(694, 22);
this.txt_folder.TabIndex = 4;
//
// btn_resetFolder
//
this.btn_resetFolder.Location = new System.Drawing.Point(713, 41);
this.btn_resetFolder.Name = "btn_resetFolder";
this.btn_resetFolder.Size = new System.Drawing.Size(75, 23);
this.btn_resetFolder.TabIndex = 5;
this.btn_resetFolder.Text = "Reset";
this.btn_resetFolder.UseVisualStyleBackColor = true;
this.btn_resetFolder.Click += new System.EventHandler(this.btn_resetFolder_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 486);
this.Controls.Add(this.btn_resetFolder);
this.Controls.Add(this.txt_folder);
this.Controls.Add(this.btn_Retry);
this.Controls.Add(this.lv_Tasks);
this.Controls.Add(this.txt_url);
this.Controls.Add(this.btn_Add);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button btn_Add;
private System.Windows.Forms.TextBox txt_url;
private System.Windows.Forms.ListView lv_Tasks;
private System.Windows.Forms.ColumnHeader columnHeader1;
private System.Windows.Forms.ColumnHeader columnHeader2;
private System.Windows.Forms.Button btn_Retry;
private System.Windows.Forms.TextBox txt_folder;
private System.Windows.Forms.Button btn_resetFolder;
}
}

219
EHDownloader/Form1.cs Normal file
View File

@@ -0,0 +1,219 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace EHDownloader
{
public partial class Form1 : System.Windows.Forms.Form
{
string baseFolder;
string _folder;
static System.Text.RegularExpressions.Regex _regex_url = new System.Text.RegularExpressions.Regex(@"^https?://e-hentai.org/g/[^/]*/[^/]*/?$",
System.Text.RegularExpressions.RegexOptions.Compiled);
static System.Text.RegularExpressions.Regex _ex_regex_url = new System.Text.RegularExpressions.Regex(@"^https?://exhentai.org/g/[^/]*/[^/]*/?$",
System.Text.RegularExpressions.RegexOptions.Compiled);
System.Threading.Thread _clipboardMonitorThread;
bool _clipboardMonitorThreadWorking = false;
DbContext _dbContext;
public Form1()
{
InitializeComponent();
baseFolder = Properties.Settings.Default.BaseFolder;
resetFolder();
_dbContext = new DbContext();
_clipboardMonitorThread = new System.Threading.Thread(clipboardMonitorProc);
_clipboardMonitorThread.SetApartmentState(System.Threading.ApartmentState.STA);
_clipboardMonitorThread.Start();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
_clipboardMonitorThreadWorking = false;
}
bool isEhUrl(string url)
{
return _regex_url.IsMatch(url) || _ex_regex_url.IsMatch(url);
}
private void clipboardMonitorProc()
{
_clipboardMonitorThreadWorking = true;
string lastText = string.Empty;
while (!IsDisposed && _clipboardMonitorThreadWorking)
{
try
{
var clipboardText = Clipboard.GetText();
if (lastText != clipboardText &&
!string.IsNullOrWhiteSpace(clipboardText))
{
lastText = clipboardText;
var lines = clipboardText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
foreach (var url in lines)
{
if (isEhUrl(url))
{
BeginInvoke(new Action(() => AddTask(url)));
}
}
}
System.Threading.Thread.Sleep(1000);
}
catch (Exception)
{
}
}
}
private void btn_Add_Click(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(txt_url.Text))
{
AddTask(txt_url.Text);
}
// await Book.DownloadBookAsync("https://e-hentai.org/g/1464116/fa326cdb2b/", @"Z:\");
//await Book.GetBookAsync("https://e-hentai.org/g/1464871/56158ca706/");
// await Book.GetBookAsync("https://e-hentai.org/g/1463748/3a40615ef7");
// Book.GetBook("https://e-hentai.org/g/1411798/ac15344281/");
}
bool downloading = false;
// object downloadingLck = new object();
// System.Threading.Mutex downloadingMutex = new System.Threading.Mutex(true);
private async void AddTask(string url)
{
if (!isEhUrl(url)) return;
if (lv_Tasks.Items.ContainsKey(url)) return;
if (_dbContext.PageIsExists(url)) return;
ListViewItem lvi = new ListViewItem(url) { Name = url };
lvi.SubItems.Add("Wait");
Book book = new Book(url, _folder);
lvi.Text = url;
lvi.Tag = book;
lv_Tasks.Items.Add(lvi);
await TryStartTask(lvi);
}
private async Task TryStartTask()
=> await TryStartTask(lv_Tasks.Items[0]);
private async Task TryStartTask(ListViewItem lvi)
{
if (downloading)
return;
try
{
downloading = true;
Book book = (Book)lvi.Tag;
bool running = true;
while (running)
{
lvi.SubItems[1].Text = book.BookState.GetDescription();
switch (book.BookState)
{
case BookStates.Wait:
await book.FetchAsync();
break;
case BookStates.FetchCompleted:
await book.DownloadBookAsync(new Progress<DownloadProgressInfo>(
(info) => lvi.SubItems[1].Text = $"下載中 {info.PageNumber}/{info.PageCount}"));
break;
case BookStates.DownloadCompleted:
await book.CompressAsync();
_dbContext.InsertPage(book);
goto default;
case BookStates.Compressed:
case BookStates.LoadFailed:
case BookStates.FetchTitleFailed:
case BookStates.FetchPageSetFailed:
case BookStates.FetchPageLinkFailed:
case BookStates.DownloadFinishedButSomePageError:
default:
var i = lv_Tasks.Items.IndexOf(lvi) + 1;
if (i >= lv_Tasks.Items.Count) return;
lvi = lv_Tasks.Items[i];
book = (Book)lvi.Tag;
break;
}
}
}
finally
{
downloading = false;
}
}
private async void Btn_Retry_Click(object sender, EventArgs e)
{
if (downloading)
return;
for (int i = 0; i < lv_Tasks.Items.Count; i++)
{
var lvi = lv_Tasks.Items[i];
var book = (Book)lvi.Tag;
switch (book.BookState)
{
case BookStates.Wait:
case BookStates.FetchCompleted:
case BookStates.DownloadCompleted:
case BookStates.Compressed:
default:
continue;
case BookStates.LoadFailed:
case BookStates.FetchTitleFailed:
case BookStates.FetchPageSetFailed:
case BookStates.FetchPageLinkFailed:
book.BookState = BookStates.Wait;
continue;
case BookStates.DownloadFinishedButSomePageError:
book.BookState = BookStates.FetchCompleted;
continue;
}
}
await TryStartTask();
}
private void setFolder(string folder)
{
_folder = folder;
txt_folder.Text = folder;
}
void resetFolder()
{
setFolder(System.IO.Path.Combine(baseFolder, DateTime.Today.ToString("yyMMdd")));
}
private void btn_resetFolder_Click(object sender, EventArgs e)
{
resetFolder();
}
}
}

120
EHDownloader/Form1.resx Normal file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using HtmlAgilityPack;
namespace EHDownloader
{
interface IHtmlDocumentProvider
{
HtmlDocument GetDocument(string url);
Task<HtmlDocument> GetDocumentAsync(string url);
}
class HtmlDocument_DirectLoader : IHtmlDocumentProvider
{
public HtmlDocument GetDocument(string url)
{
HtmlWeb htmlWeb = GetHtmlWeb();
return htmlWeb.Load(url);
}
public Task<HtmlDocument> GetDocumentAsync(string url)
{
HtmlWeb htmlWeb = GetHtmlWeb();
return htmlWeb.LoadFromWebAsync(url);
}
private static HtmlWeb GetHtmlWeb()
{
HtmlWeb htmlWeb = new HtmlWeb();
htmlWeb.PreRequest +=
(request) =>
{
request.CookieContainer = WebHelper.GetCookieContainer();
request.UserAgent = WebHelper.UserAgent;
return true;
};
return htmlWeb;
}
}
class HtmlDocument_HttpWebRequest : IHtmlDocumentProvider
{
public HtmlDocument GetDocument(string url)
{
HttpWebRequest request = GetRequest(url);
var response = request.GetResponse();
var stream = response.GetResponseStream();
var doc = new HtmlDocument();
doc.Load(stream);
return doc;
}
public async Task<HtmlDocument> GetDocumentAsync(string url)
{
HttpWebRequest request = GetRequest(url);
var response = await request.GetResponseAsync();
var stream = response.GetResponseStream();
var doc = new HtmlDocument();
doc.Load(stream);
return doc;
}
private static HttpWebRequest GetRequest(string url)
{
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.UserAgent = WebHelper.UserAgent;
request.CookieContainer = WebHelper.GetCookieContainer();
return request;
}
}
class HtmlDocument_HttpClient : IHtmlDocumentProvider
{
public HtmlDocument GetDocument(string url)
{
return GetDocumentAsync(url).Result;
}
public async Task<HtmlDocument> GetDocumentAsync(string url)
{
HttpClient client = GetHttpClient();
//var response = await client.GetAsync(url);
//if (!response.IsSuccessStatusCode)
//{
//}
//String urlContents = await response.Content.ReadAsStringAsync();
var doc = new HtmlDocument();
doc.LoadHtml(await client.GetStringAsync(url));
return doc;
}
private static HttpClient GetHttpClient()
{
HttpClientHandler handler = new HttpClientHandler()
{
UseCookies = true,
CookieContainer = WebHelper.GetCookieContainer(),
};
HttpClient client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("user-agent", WebHelper.UserAgent);
return client;
}
}
}

View File

@@ -0,0 +1,82 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader
{
static class WebHelper
{
public static HtmlWeb HtmlWeb;
private static CookieContainer _cookieContainer;
public static HttpClient HttpClient = new HttpClient();
// static public HtmlWeb GetInstance() => _htmlWeb;
static WebHelper()
{
_cookieContainer = new CookieContainer();
HtmlWeb = new HtmlWeb() { UseCookies = false };
HtmlWeb.PreRequest += new HtmlWeb.PreRequestHandler(preRequest);
var handler = new HttpClientHandler()
{
CookieContainer = _cookieContainer,
UseCookies = true,
};
HttpClient = new HttpClient(handler);
HttpClient.DefaultRequestHeaders.Add("user-agent", UserAgent);
_cookieContainer.Add(new Cookie("ipb_member_id", Properties.Settings.Default.ipb_member_id, "/", ".exhentai.org"));
_cookieContainer.Add(new Cookie("ipb_pass_hash", Properties.Settings.Default.ipb_pass_hash, "/", ".exhentai.org"));
//_cookieContainer.Add(new Cookie("igneous", "1794dbb8a", "/", ".exhentai.org"));
//_cookieContainer.Add(new Cookie("sk", "268qob5mp4g4vh9vbhi8l7wmme0p", "/", ".exhentai.org"));
}
static public CookieContainer GetCookieContainer()
{
return _cookieContainer;
}
static public string UserAgent => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36";
static private bool preRequest(HttpWebRequest request)
{
request.CookieContainer = _cookieContainer;
request.UserAgent = UserAgent;
return true;
}
static public HtmlDocument Load(string url)
{
return HtmlWeb.Load(url);
}
public static async Task<HtmlDocument> LoadAsync(string url)
{
return await HtmlWeb.LoadFromWebAsync(url);
}
static public void Download()
{
}
//static private WebClient _instance = new WebClient();
//static public WebClient GetInstance() => _instance;
//static public HtmlDocument Load(string url)
//{
// return _instance.D(url);
//}
}
}

158
EHDownloader/Page.cs Normal file
View File

@@ -0,0 +1,158 @@
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader
{
enum DownloadPageResults
{
WaitToDownload,
Downloading,
Completed,
LoadPageHtmlFailed,
DownloadImageResponseError,
DownloadImageError,
}
class Page
{
public string Url { get; private set; }
public int PageNumber { get; internal set; }
public string FileName => PageNumber.ToString("00000");
public DownloadPageResults Result { get; private set; } = DownloadPageResults.WaitToDownload;
IHtmlDocumentProvider _htmlDocProvider = new HtmlDocument_HttpClient();
public Page(string url)
{
Url = url;
}
internal async Task DownloadToAsync(FileContainer.FileContainerBase fileContainer)
{
HtmlDocument doc;
string imgUrl;
try
{
doc = await _htmlDocProvider.GetDocumentAsync(Url);
var imgElem = doc.DocumentNode.SelectNodes(@"//*[@id='img']").FirstOrDefault();
if (imgElem == null)
{
throw new Exception($"Fetch img error: {Url}");
}
imgUrl = imgElem.Attributes["src"].Value;
}
catch
{
Result = DownloadPageResults.LoadPageHtmlFailed;
return;
}
try
{
var response = await WebHelper.HttpClient.GetAsync(imgUrl);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
System.Diagnostics.Debug.WriteLine($"Response is not OK: {Url}, {response.StatusCode}");
Result = DownloadPageResults.DownloadImageResponseError;
return;
}
string ext = ".jpg";
switch (response.Content.Headers.ContentType.MediaType)
{
case "image/jpeg":
ext = ".jpg";
break;
case "image/png":
ext = ".png";
break;
case "image/gif":
ext = ".gif";
break;
}
using (var memoryStream = new System.IO.MemoryStream())
{
await response.Content.CopyToAsync(memoryStream);
memoryStream.Seek(0, System.IO.SeekOrigin.Begin);
using (var fileStream = fileContainer.CreateFile(FileName + ext))
await memoryStream.CopyToAsync(fileStream);
Result = DownloadPageResults.Completed;
}
}
catch
{
Result = DownloadPageResults.DownloadImageError;
return;
}
}
internal async Task DownloadToAsync(string folder)
{
int retryTimes = 0;
int retryLimit = 5;
while (retryTimes < retryLimit)
{
// "skipserver=31908-18135"
var doc = await WebHelper.LoadAsync(Url);
var imgElem = doc.DocumentNode.SelectNodes(@"//*[@id='img']").FirstOrDefault();
if (imgElem == null)
{
System.Diagnostics.Debug.WriteLine($"Fetch img error: {Url}");
return;
}
var imgUrl = imgElem.Attributes["src"].Value;
try
{
var response = await WebHelper.HttpClient.GetAsync(imgUrl);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
System.Diagnostics.Debug.WriteLine($"Response is not OK: {Url}, {response.StatusCode}");
return;
}
string ext = ".jpg";
switch (response.Content.Headers.ContentType.MediaType)
{
case "image/jpeg":
ext = ".jpg";
break;
case "image/png":
ext = ".png";
break;
case "image/gif":
ext = ".gif";
break;
}
string path = System.IO.Path.Combine(folder, FileName + ext);
using (var fs = System.IO.File.Create(path))
await response.Content.CopyToAsync(fs);
break;
}
catch
{
if (retryTimes++ >= retryLimit)
{
System.Diagnostics.Debug.WriteLine($"Download image failed, skip: {Url}");
}
else
{
System.Diagnostics.Debug.WriteLine($"Download image failed, retry{retryTimes}/{retryLimit}: {Url}");
}
}
}
}
}
}

23
EHDownloader/Program.cs Normal file
View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace EHDownloader
{
static class Program
{
/// <summary>
/// 應用程式的主要進入點。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 組件的一般資訊是由下列的屬性集控制。
// 變更這些屬性的值即可修改組件的相關
// 資訊。
[assembly: AssemblyTitle("EHDownloader")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("EHDownloader")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 將 ComVisible 設為 false 可對 COM 元件隱藏
// 組件中的類型。若必須從 COM 存取此組件中的類型,
// 的類型,請在該類型上將 ComVisible 屬性設定為 true。
[assembly: ComVisible(false)]
// 下列 GUID 為專案公開 (Expose) 至 COM 時所要使用的 typelib ID
[assembly: Guid("824f3ce9-87f6-48e0-b9d6-d5f19dc33960")]
// 組件的版本資訊由下列四個值所組成:
//
// 主要版本
// 次要版本
// 組建編號
// 修訂編號
//
// 您可以指定所有的值,也可以使用 '*' 將組建和修訂編號
// 設為預設,如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 這段程式碼是由工具產生的。
// 執行階段版本:4.0.30319.42000
//
// 變更這個檔案可能會導致不正確的行為,而且如果已重新產生
// 程式碼,則會遺失變更。
// </auto-generated>
//------------------------------------------------------------------------------
namespace EHDownloader.Properties
{
/// <summary>
/// 用於查詢當地語系化字串等的強類型資源類別
/// </summary>
// 這個類別是自動產生的,是利用 StronglyTypedResourceBuilder
// 類別透過 ResGen 或 Visual Studio 這類工具產生。
// 若要加入或移除成員,請編輯您的 .ResX 檔,然後重新執行 ResGen
// (利用 /str 選項),或重建您的 VS 專案。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// 傳回這個類別使用的快取的 ResourceManager 執行個體。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EHDownloader.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 覆寫目前執行緒的 CurrentUICulture 屬性,對象是所有
/// 使用這個強類型資源類別的資源查閱。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 這段程式碼是由工具產生的。
// 執行階段版本:4.0.30319.42000
//
// 對這個檔案所做的變更可能會造成錯誤的行為,而且如果重新產生程式碼,
// 變更將會遺失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace EHDownloader.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.7.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("D:\\books")]
public string BaseFolder {
get {
return ((string)(this["BaseFolder"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("1512454")]
public string ipb_member_id {
get {
return ((string)(this["ipb_member_id"]));
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("6b31380b7ce97c6d455772ea3ead014f")]
public string ipb_pass_hash {
get {
return ((string)(this["ipb_pass_hash"]));
}
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="EHDownloader.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="BaseFolder" Type="System.String" Scope="Application">
<Value Profile="(Default)">D:\books</Value>
</Setting>
<Setting Name="ipb_member_id" Type="System.String" Scope="Application">
<Value Profile="(Default)">1512454</Value>
</Setting>
<Setting Name="ipb_pass_hash" Type="System.String" Scope="Application">
<Value Profile="(Default)">6b31380b7ce97c6d455772ea3ead014f</Value>
</Setting>
</Settings>
</SettingsFile>

29
EHDownloader/Settings.cs Normal file
View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace EHDownloader
{
class Settings
{
static public Settings Instance { get; } = new Settings();
public bool DeleteDownloadFolderAfterCompressed { get; set; } = true;
internal static string GetDbPath()
{
return @".\db.sqlite";
//return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "db.sqlite");
}
internal static string GetConnectString()
{
return $"data source={GetDbPath()}";
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Dapper" version="2.0.4" targetFramework="net472" />
<package id="HtmlAgilityPack" version="1.11.12" targetFramework="net472" />
<package id="System.Data.SQLite.Core" version="1.0.111.0" targetFramework="net472" />
<package id="System.Reflection.Emit.Lightweight" version="4.3.0" targetFramework="net472" />
</packages>