212 lines
7.5 KiB
C#
212 lines
7.5 KiB
C#
using System.Diagnostics;
|
||
using System.Net;
|
||
using PdfSharp.Drawing;
|
||
using PdfSharp.Pdf;
|
||
|
||
namespace WSiPBookDownloader;
|
||
|
||
internal class BookMissingException : Exception
|
||
{
|
||
public uint BookId { get; }
|
||
|
||
public BookMissingException(uint 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)
|
||
{
|
||
var onlyCovers = args.Any(arg => arg.Equals("--only-covers", StringComparison.OrdinalIgnoreCase));
|
||
|
||
var outputDirectory = Directory.GetCurrentDirectory();
|
||
var outputDirectoryIndex = Array.FindIndex(args, arg => arg.Equals("--output-dir", StringComparison.OrdinalIgnoreCase));
|
||
if (outputDirectoryIndex > -1)
|
||
{
|
||
if (outputDirectoryIndex + 1 >= args.Length)
|
||
{
|
||
Log("--output-dir requires a value.", ConsoleColor.DarkRed);
|
||
return;
|
||
}
|
||
|
||
outputDirectory = args[outputDirectoryIndex + 1];
|
||
|
||
if (!Directory.Exists(outputDirectory))
|
||
{
|
||
Log($"Output directory \"{outputDirectory}\" does not exist.", ConsoleColor.DarkRed);
|
||
return;
|
||
}
|
||
}
|
||
|
||
var positionalArgs = args
|
||
.Except(
|
||
[
|
||
"--only-covers",
|
||
"--output-dir",
|
||
outputDirectory
|
||
], StringComparer.OrdinalIgnoreCase)
|
||
.ToArray();
|
||
|
||
var startBookId = int.Parse(positionalArgs[0]);
|
||
var endBookId = positionalArgs.Length > 1
|
||
? int.Parse(positionalArgs[1])
|
||
: startBookId;
|
||
|
||
var bookIds = Enumerable.Range(startBookId, endBookId - startBookId + 1).Select(bookId => (uint)bookId);
|
||
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = onlyCovers ? 100 : 10 };
|
||
|
||
Log($"Output directory: \"{outputDirectory}\"");
|
||
|
||
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(uint 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(uint 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(uint 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);
|
||
}
|
||
} |