diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f20901c --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..675566d --- /dev/null +++ b/Program.cs @@ -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); + } +} \ No newline at end of file diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..1407c55 --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,16 @@ + + + + + Release + Any CPU + bin\Release\net10.0\win-x64\publish\win-x64\ + FileSystem + <_TargetId>Folder + net10.0 + win-x64 + true + false + false + + \ No newline at end of file diff --git a/WSiPBookDownloader.csproj b/WSiPBookDownloader.csproj new file mode 100644 index 0000000..89383bb --- /dev/null +++ b/WSiPBookDownloader.csproj @@ -0,0 +1,17 @@ + + + + Exe + net10.0 + true + true + win-x64 + enable + enable + + + + + + + diff --git a/WSiPBookDownloader.slnx b/WSiPBookDownloader.slnx new file mode 100644 index 0000000..218b9ca --- /dev/null +++ b/WSiPBookDownloader.slnx @@ -0,0 +1,3 @@ + + +