Add project files
This commit is contained in:
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
## A streamlined .gitignore for modern .NET projects
|
||||||
|
## including temporary files, build results, and
|
||||||
|
## files generated by popular .NET tools. If you are
|
||||||
|
## developing with Visual Studio, the VS .gitignore
|
||||||
|
## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||||
|
## has more thorough IDE-specific entries.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Ww][Ii][Nn]32/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# ASP.NET Scaffolding
|
||||||
|
ScaffoldingReadMe.txt
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# NuGet Symbol Packages
|
||||||
|
*.snupkg
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Others
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
CodeCoverage/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUnit
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
nunit-*.xml
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
Properties/launchsettings.json
|
||||||
|
*.user
|
||||||
194
Program.cs
Normal file
194
Program.cs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using PdfSharp.Drawing;
|
||||||
|
using PdfSharp.Pdf;
|
||||||
|
|
||||||
|
namespace WSiPBookDownloader;
|
||||||
|
|
||||||
|
internal class BookMissingException : Exception
|
||||||
|
{
|
||||||
|
public int BookId { get; }
|
||||||
|
|
||||||
|
public BookMissingException(int bookId)
|
||||||
|
: base($"Book with ID {bookId} does not exist.")
|
||||||
|
{
|
||||||
|
BookId = bookId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
private static readonly HttpClient httpClient = new
|
||||||
|
(
|
||||||
|
new HttpClientHandler
|
||||||
|
{
|
||||||
|
AllowAutoRedirect = false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("https://appwsipnet.eduranga.pl/"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Lock _consoleLock = new();
|
||||||
|
|
||||||
|
private static void Log(string message, ConsoleColor? color = null)
|
||||||
|
{
|
||||||
|
lock (_consoleLock)
|
||||||
|
{
|
||||||
|
if (color.HasValue)
|
||||||
|
Console.ForegroundColor = color.Value;
|
||||||
|
|
||||||
|
Console.WriteLine(message);
|
||||||
|
Console.ResetColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 2 || args.Length > 4)
|
||||||
|
{
|
||||||
|
Log("Usage: [outputDirectory] [startBookId] [endBookId] [--only-covers]", ConsoleColor.DarkRed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputDirectory = args[0];
|
||||||
|
if (!Path.Exists(outputDirectory))
|
||||||
|
{
|
||||||
|
Log($"Output directory \"{outputDirectory}\" does not exist.", ConsoleColor.DarkRed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startBookId = int.Parse(args[1]);
|
||||||
|
var endBookId = (args.Length >= 3 && int.TryParse(args[2], out var parsedEndBookId))
|
||||||
|
? parsedEndBookId
|
||||||
|
: startBookId;
|
||||||
|
var onlyCovers = args.FirstOrDefault(arg => arg.Equals("--only-covers", StringComparison.OrdinalIgnoreCase)) != null;
|
||||||
|
|
||||||
|
var bookIds = Enumerable.Range(startBookId, endBookId - startBookId + 1);
|
||||||
|
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = onlyCovers ? 100 : 10 };
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(bookIds, parallelOptions, async (bookId, cancellationToken) =>
|
||||||
|
{
|
||||||
|
var outputPath = Path.Combine(outputDirectory, $"{bookId}.pdf");
|
||||||
|
if (File.Exists(outputPath))
|
||||||
|
{
|
||||||
|
Log($"[!] Book with ID {bookId} already downloaded, skipping...", ConsoleColor.Yellow);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"[?] Starting download of book with ID {bookId}", ConsoleColor.Cyan);
|
||||||
|
var jpgDir = Path.Combine(Path.GetTempPath(), "WSiPBookDownloader", bookId.ToString());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (onlyCovers)
|
||||||
|
{
|
||||||
|
await DownloadBookCoverJpg(bookId, outputDirectory, cancellationToken);
|
||||||
|
Log($"[✓] Downloaded book cover {bookId}", ConsoleColor.DarkGreen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DownloadBookJpg(bookId, jpgDir, cancellationToken);
|
||||||
|
BuildPdf(bookId, jpgDir, outputPath);
|
||||||
|
|
||||||
|
Log($"[✓] Downloaded book {bookId}", ConsoleColor.DarkGreen);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log($"[×] Failed to download book {bookId}:\n {ex.Message}", ConsoleColor.DarkRed);
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(jpgDir))
|
||||||
|
Directory.Delete(jpgDir, recursive: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DownloadBookCoverJpg(int bookId, string outputDir, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var outputPath = Path.Combine(outputDir, $"{bookId}.jpg");
|
||||||
|
if (File.Exists(outputPath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Cover for book {bookId} already exists, skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync($"e-podreczniki/podglad/{bookId}/files/mobile/1.jpg", cancellationToken);
|
||||||
|
if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Found)
|
||||||
|
throw new BookMissingException(bookId);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
Directory.CreateDirectory(outputDir);
|
||||||
|
await File.WriteAllBytesAsync(outputPath, content, cancellationToken);
|
||||||
|
|
||||||
|
Debug.WriteLine($"Downloaded cover for book {bookId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DownloadBookJpg(int bookId, string outputDir, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var currentPage = 1;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var outputPath = Path.Combine(outputDir, $"{currentPage}.jpg");
|
||||||
|
if (File.Exists(outputPath))
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Page {currentPage} of book {bookId} already exists, skipping...");
|
||||||
|
|
||||||
|
currentPage++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync($"e-podreczniki/podglad/{bookId}/files/mobile/{currentPage}.jpg", cancellationToken);
|
||||||
|
if (
|
||||||
|
(response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.NotFound) &&
|
||||||
|
currentPage > 1
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"No more pages for book {bookId}, stopping...");
|
||||||
|
break;
|
||||||
|
} else if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Found)
|
||||||
|
throw new BookMissingException(bookId);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
Directory.CreateDirectory(outputDir);
|
||||||
|
await File.WriteAllBytesAsync(outputPath, content, cancellationToken);
|
||||||
|
|
||||||
|
Debug.WriteLine($"Downloaded page {currentPage} of book {bookId}");
|
||||||
|
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildPdf(int bookId, string jpgDir, string outputPath)
|
||||||
|
{
|
||||||
|
var pages = Directory.GetFiles(jpgDir, "*.jpg")
|
||||||
|
.OrderBy(file => int.Parse(Path.GetFileNameWithoutExtension(file)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
using var document = new PdfDocument();
|
||||||
|
document.Options.FlateEncodeMode = PdfFlateEncodeMode.BestCompression;
|
||||||
|
document.Options.UseFlateDecoderForJpegImages = PdfUseFlateDecoderForJpegImages.Never;
|
||||||
|
|
||||||
|
foreach (var pagePath in pages)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(File.ReadAllBytes(pagePath));
|
||||||
|
using var image = XImage.FromStream(stream);
|
||||||
|
|
||||||
|
var pdfPage = document.AddPage();
|
||||||
|
pdfPage.Width = XUnit.FromPoint(image.PointWidth);
|
||||||
|
pdfPage.Height = XUnit.FromPoint(image.PointHeight);
|
||||||
|
|
||||||
|
using var gfx = XGraphics.FromPdfPage(pdfPage);
|
||||||
|
gfx.DrawImage(image, 0, 0, image.PointWidth, image.PointHeight);
|
||||||
|
|
||||||
|
Debug.WriteLine($"Added page {Path.GetFileNameWithoutExtension(pagePath)} to PDF for book {bookId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||||
|
document.Save(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
16
Properties/PublishProfiles/FolderProfile.pubxml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>Any CPU</Platform>
|
||||||
|
<PublishDir>bin\Release\net10.0\win-x64\publish\win-x64\</PublishDir>
|
||||||
|
<PublishProtocol>FileSystem</PublishProtocol>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<PublishReadyToRun>false</PublishReadyToRun>
|
||||||
|
<PublishTrimmed>false</PublishTrimmed>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
17
WSiPBookDownloader.csproj
Normal file
17
WSiPBookDownloader.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
<SelfContained>true</SelfContained>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="PDFsharp" Version="6.2.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
3
WSiPBookDownloader.slnx
Normal file
3
WSiPBookDownloader.slnx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="WSiPBookDownloader.csproj" />
|
||||||
|
</Solution>
|
||||||
Reference in New Issue
Block a user