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); } }