looks better i guess

master
Avril 4 years ago
parent 5cc997d0e5
commit fb70eb041e
Signed by: flanchan
GPG Key ID: 284488987C31F630

@ -1,351 +1,351 @@
using AngleSharp; using AngleSharp;
using AngleSharp.Dom; using AngleSharp.Dom;
using AngleSharp.Io; using AngleSharp.Io;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Linq; using System.Linq;
namespace napdump.Dumpers namespace napdump.Dumpers
{ {
class Nineball : Dumper class Nineball : Dumper
{ {
readonly IConfiguration browserConfig; readonly IConfiguration browserConfig;
readonly IBrowsingContext context; readonly IBrowsingContext context;
readonly ICookieProvider cookies; readonly ICookieProvider cookies;
public Nineball(DumperConfig config) : base(config) public Nineball(DumperConfig config) : base(config)
{ {
browserConfig = Configuration.Default.WithDefaultCookies().WithDefaultLoader(); browserConfig = Configuration.Default.WithDefaultCookies().WithDefaultLoader();
cookies = browserConfig.Services.OfType<ICookieProvider>().First(); cookies = browserConfig.Services.OfType<ICookieProvider>().First();
foreach (var c in config.Cookies ?? Array.Empty<(string Url, string Value)>()) foreach (var c in config.Cookies ?? Array.Empty<(string Url, string Value)>())
{ {
cookies.SetCookie(new Url(c.Url), c.Value); cookies.SetCookie(new Url(c.Url), c.Value);
} }
context = BrowsingContext.New(browserConfig); context = BrowsingContext.New(browserConfig);
} }
private static readonly Regex reBoardName = new Regex(@"^(\/.*?\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex reBoardName = new Regex(@"^(\/.*?\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex reBoardNameW = new Regex(@"^\/(\w+)\/", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex reBoardNameW = new Regex(@"^\/(\w+)\/", RegexOptions.Compiled | RegexOptions.IgnoreCase);
protected async Task GetBoardInfo(BoardInfo bi, IDocument document, CancellationToken token) protected async Task GetBoardInfo(BoardInfo bi, IDocument document, CancellationToken token)
{ {
await Task.Yield(); await Task.Yield();
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
bi.Title = document.QuerySelector("body > threads > h1").InnerHtml; bi.Title = document.QuerySelector("body > threads > h1").InnerHtml;
bi.BoardName = reBoardName.IsMatch(bi.Title) ? reBoardName.Match(bi.Title).Groups[1].Value : bi.Title; bi.BoardName = reBoardName.IsMatch(bi.Title) ? reBoardName.Match(bi.Title).Groups[1].Value : bi.Title;
bi.SafeName = reBoardNameW.IsMatch(bi.Title) ? reBoardNameW.Match(bi.Title).Groups[1].Value : "unbound"; bi.SafeName = reBoardNameW.IsMatch(bi.Title) ? reBoardNameW.Match(bi.Title).Groups[1].Value : "unbound";
bi.Description = document.QuerySelector("#banner_info").TextContent; bi.Description = document.QuerySelector("#banner_info").TextContent;
bi.Tags = new[] { "meguca", "node", "liveboard" }; bi.Tags = new[] { "meguca", "node", "liveboard" };
} }
private static readonly Regex reImageDim = new Regex(@"\((\d+) ([kmg]?b), (\d+)x(\d+)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex reImageDim = new Regex(@"\((\d+) ([kmg]?b), (\d+)x(\d+)\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseImageDimInfo(string info, out long size, out int x, out int y) private static bool TryParseImageDimInfo(string info, out long size, out int x, out int y)
{ {
//Console.WriteLine(info + " " + reImageDim.IsMatch(info)); //Console.WriteLine(info + " " + reImageDim.IsMatch(info));
if(reImageDim.IsMatch(info)) if(reImageDim.IsMatch(info))
{ {
var groups = reImageDim.Match(info).Groups; var groups = reImageDim.Match(info).Groups;
if(long.TryParse(groups[1].Value, out var rawSize) && if(long.TryParse(groups[1].Value, out var rawSize) &&
int.TryParse(groups[3].Value, out x) && int.TryParse(groups[3].Value, out x) &&
int.TryParse(groups[4].Value, out y)) int.TryParse(groups[4].Value, out y))
{ {
long multiplier = 1; long multiplier = 1;
switch (groups[2].Value.ToLower().Trim()) switch (groups[2].Value.ToLower().Trim())
{ {
case "b": case "b":
break; break;
case "kb": case "kb":
multiplier = 1024; multiplier = 1024;
break; break;
case "mb": case "mb":
multiplier = 1024 * 1024; multiplier = 1024 * 1024;
break; break;
case "gb": case "gb":
multiplier = 1024 * 1024 * 1024; multiplier = 1024 * 1024 * 1024;
break; break;
default: default:
goto bad; goto bad;
} }
size = rawSize & multiplier; size = rawSize & multiplier;
return true; return true;
} }
} }
bad: bad:
size = default; size = default;
x = y = default; x = y = default;
return false; return false;
} }
private static readonly Regex reDateTime = new Regex(@"(\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d\d\d\d)\(\w+\)(\d\d):(\d\d)", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Regex reDateTime = new Regex(@"(\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d\d\d\d)\(\w+\)(\d\d):(\d\d)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static bool TryParseDateTime(string htmlDateTime, out DateTime dt) private static bool TryParseDateTime(string htmlDateTime, out DateTime dt)
{ {
htmlDateTime = htmlDateTime.Trim(); htmlDateTime = htmlDateTime.Trim();
//Console.WriteLine(htmlDateTime + " " + reDateTime.IsMatch(htmlDateTime)); //Console.WriteLine(htmlDateTime + " " + reDateTime.IsMatch(htmlDateTime));
if(reDateTime.IsMatch(htmlDateTime)) if(reDateTime.IsMatch(htmlDateTime))
{ {
var groups = reDateTime.Match(htmlDateTime).Groups; var groups = reDateTime.Match(htmlDateTime).Groups;
int day = int.Parse(groups[1].Value); int day = int.Parse(groups[1].Value);
string month = groups[2].Value; string month = groups[2].Value;
int year = int.Parse(groups[3].Value); int year = int.Parse(groups[3].Value);
int hour = int.Parse(groups[4].Value); int hour = int.Parse(groups[4].Value);
int minute = int.Parse(groups[5].Value); int minute = int.Parse(groups[5].Value);
try try
{ {
dt = new DateTime(year, month switch dt = new DateTime(year, month switch
{ {
"Jan" => 1, "Jan" => 1,
"Feb" => 2, "Feb" => 2,
"Mar" => 3, "Mar" => 3,
"Apr" => 4, "Apr" => 4,
"May" => 5, "May" => 5,
"Jun" => 6, "Jun" => 6,
"Jul" => 7, "Jul" => 7,
"Aug" => 8, "Aug" => 8,
"Sep" => 9, "Sep" => 9,
"Oct" => 10, "Oct" => 10,
"Nov" => 11, "Nov" => 11,
"Dec" => 12, "Dec" => 12,
_ => throw new InvalidDataException(), _ => throw new InvalidDataException(),
}, day, hour, minute, 0); }, day, hour, minute, 0);
return true; return true;
} }
catch catch
{ {
dt = default; dt = default;
return false; return false;
} }
} }
dt = default; dt = default;
return false; return false;
} }
private static readonly Regex reImageDeleted = new Regex(@"^Image deleted by (\w+)$", RegexOptions.Compiled); private static readonly Regex reImageDeleted = new Regex(@"^Image deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reImageSpoilered = new Regex(@"^Image spoilered by (\w+)$", RegexOptions.Compiled); private static readonly Regex reImageSpoilered = new Regex(@"^Image spoilered by (\w+)$", RegexOptions.Compiled);
private static readonly Regex rePostDeleted = new Regex(@"^Post deleted by (\w+)$", RegexOptions.Compiled); private static readonly Regex rePostDeleted = new Regex(@"^Post deleted by (\w+)$", RegexOptions.Compiled);
private static readonly Regex reUserBanned = new Regex(@"^User banned by (\w+)(?: for (.+))?$", RegexOptions.Compiled); private static readonly Regex reUserBanned = new Regex(@"^User banned by (\w+)(?: for (.+))?$", RegexOptions.Compiled);
private static void getModlog(string nodeHtml, out Modlog log) private static void getModlog(string nodeHtml, out Modlog log)
{ {
log = new Modlog(); log = new Modlog();
if (nodeHtml == null) return; if (nodeHtml == null) return;
try try
{ {
var split = nodeHtml.Split("<br>").Select(x => x.Trim()).Where(x=> x.Length>0); var split = nodeHtml.Split("<br>").Select(x => x.Trim()).Where(x=> x.Length>0);
foreach (var line in split) foreach (var line in split)
{ {
if (reImageDeleted.IsMatch(line)) if (reImageDeleted.IsMatch(line))
log.ImageDeleted = AdminInfo.Create(true, reImageDeleted.Match(line).Groups[1].Value); log.ImageDeleted = AdminInfo.Create(true, reImageDeleted.Match(line).Groups[1].Value);
if (reImageSpoilered.IsMatch(line)) if (reImageSpoilered.IsMatch(line))
log.ImageSpoilered = AdminInfo.Create(true, reImageSpoilered.Match(line).Groups[1].Value); log.ImageSpoilered = AdminInfo.Create(true, reImageSpoilered.Match(line).Groups[1].Value);
if (rePostDeleted.IsMatch(line)) if (rePostDeleted.IsMatch(line))
log.PostDeleted = AdminInfo.Create(true, rePostDeleted.Match(line).Groups[1].Value); log.PostDeleted = AdminInfo.Create(true, rePostDeleted.Match(line).Groups[1].Value);
if (reUserBanned.IsMatch(line)) if (reUserBanned.IsMatch(line))
{ {
var match = reUserBanned.Match(line).Groups; var match = reUserBanned.Match(line).Groups;
log.UserBanned = AdminInfo.Create(true, match[1].Value); log.UserBanned = AdminInfo.Create(true, match[1].Value);
if (match[2].Success) if (match[2].Success)
log.BanMessage = AdminInfo.Create(match[2].Value, match[1].Value); log.BanMessage = AdminInfo.Create(match[2].Value, match[1].Value);
} }
} }
} }
catch(Exception ex) catch(Exception ex)
{ {
Console.WriteLine("Modlog parsing error: "+ex.Message); Console.WriteLine("Modlog parsing error: "+ex.Message);
log = new Modlog(); log = new Modlog();
} }
} }
private static readonly Regex reMailTo = new Regex(@"^mailto:", RegexOptions.Compiled); private static readonly Regex reMailTo = new Regex(@"^mailto:", RegexOptions.Compiled);
protected override async IAsyncEnumerable<PostInfo> GetPosts(ThreadInfo thread, [EnumeratorCancellation] CancellationToken token) protected override async IAsyncEnumerable<PostInfo> GetPosts(ThreadInfo thread, [EnumeratorCancellation] CancellationToken token)
{ {
var document = await context.OpenAsync(thread.BoardInfo.BoardURL + thread.PostNumber, token); var document = await context.OpenAsync(thread.BoardInfo.BoardURL + thread.PostNumber, token);
var section = document.QuerySelector("section"); var section = document.QuerySelector("section");
thread.Locked = section.ClassList.Contains("locked"); thread.Locked = section.ClassList.Contains("locked");
(string Name, string Tripcode, string Email, string Capcode) getTripcode(IElement header) (string Name, string Tripcode, string Email, string Capcode) getTripcode(IElement header)
{ {
var bname = header.QuerySelector("b"); var bname = header.QuerySelector("b");
string name, trip, mail, cap; string name, trip, mail, cap;
name = trip = mail = cap = null; name = trip = mail = cap = null;
if(bname.FirstChild.NodeName.ToLower() == "a") if(bname.FirstChild.NodeName.ToLower() == "a")
{ {
//Mail link //Mail link
mail = bname.FirstElementChild.GetAttribute("href"); mail = bname.FirstElementChild.GetAttribute("href");
if (reMailTo.IsMatch(mail)) if (reMailTo.IsMatch(mail))
mail = reMailTo.Replace(mail, ""); mail = reMailTo.Replace(mail, "");
bname = bname.FirstElementChild; bname = bname.FirstElementChild;
} }
if(bname.ChildNodes.Length > 1 && bname.ChildNodes[1].NodeName=="CODE") if(bname.ChildNodes.Length > 1 && bname.ChildNodes[1].NodeName=="CODE")
{ {
//Has tripcode & name //Has tripcode & name
name = bname.FirstChild.TextContent; name = bname.FirstChild.TextContent;
trip = bname.ChildNodes[1].TextContent; trip = bname.ChildNodes[1].TextContent;
if (bname.ChildNodes.Length > 2) if (bname.ChildNodes.Length > 2)
cap = bname.ChildNodes[2].TextContent; cap = bname.ChildNodes[2].TextContent;
} }
else if(bname.ChildNodes.Length>1) else if(bname.ChildNodes.Length>1)
{ {
name = bname.FirstChild.TextContent; name = bname.FirstChild.TextContent;
cap = bname.ChildNodes[1].TextContent; cap = bname.ChildNodes[1].TextContent;
} }
else if(bname.FirstChild.NodeName.ToLower() == "code") else if(bname.FirstChild.NodeName.ToLower() == "code")
{ {
//Tripcode, no name. //Tripcode, no name.
trip = bname.FirstChild.TextContent; trip = bname.FirstChild.TextContent;
} }
else else
{ {
//Name, no tripcode //Name, no tripcode
name = bname.FirstChild.TextContent; name = bname.FirstChild.TextContent;
} }
return (name, trip, mail, cap); return (name, trip, mail, cap);
} }
//Get thread's modlog. //Get thread's modlog.
getModlog(section.QuerySelector("b.modLog")?.InnerHtml, out var threadModlog); getModlog(section.QuerySelector("b.modLog")?.InnerHtml, out var threadModlog);
thread.ModLog = threadModlog; thread.ModLog = threadModlog;
//Get thread's info. //Get thread's info.
var imageInfo = section.QuerySelector("figure > figcaption > i"); var imageInfo = section.QuerySelector("figure > figcaption > i");
if (imageInfo != null) if (imageInfo != null)
{ {
string imageDimInfo = imageInfo.FirstChild.TextContent; string imageDimInfo = imageInfo.FirstChild.TextContent;
if (TryParseImageDimInfo(imageDimInfo, out var _imageSize, out var _x, out var _y)) if (TryParseImageDimInfo(imageDimInfo, out var _imageSize, out var _x, out var _y))
{ {
thread.ImageSize = _imageSize; thread.ImageSize = _imageSize;
thread.ImageDimensions = (_x, _y); thread.ImageDimensions = (_x, _y);
var imageNameInfo = imageInfo.QuerySelector("a"); var imageNameInfo = imageInfo.QuerySelector("a");
thread.ImageURL = imageNameInfo.GetAttribute("href"); thread.ImageURL = imageNameInfo.GetAttribute("href");
thread.ImageFilename = imageNameInfo.GetAttribute("download"); thread.ImageFilename = imageNameInfo.GetAttribute("download");
if (TryParseDateTime(section.QuerySelector("header > time").FirstChild.TextContent, out var threadTimestamp)) if (TryParseDateTime(section.QuerySelector("header > time").FirstChild.TextContent, out var threadTimestamp))
{ {
thread.Timestamp = threadTimestamp; thread.Timestamp = threadTimestamp;
} }
else else
{ {
thread.Timestamp = default; thread.Timestamp = default;
} }
(thread.Name, thread.Tripcode, thread.Email, thread.Capcode) = getTripcode(section.QuerySelector("header")); (thread.Name, thread.Tripcode, thread.Email, thread.Capcode) = getTripcode(section.QuerySelector("header"));
} }
else else
{ {
thread.ImageDimensions = default; thread.ImageDimensions = default;
thread.ImageFilename = null; thread.ImageFilename = null;
thread.ImageSize = 0; thread.ImageSize = 0;
thread.ImageURL = null; thread.ImageURL = null;
} }
} }
thread.Body = section.QuerySelector("blockquote").InnerHtml; thread.Body = section.QuerySelector("blockquote").InnerHtml;
thread.ThreadURL = document.Url; thread.ThreadURL = document.Url;
thread.Subject = section.QuerySelector("header > h3")?.TextContent; thread.Subject = section.QuerySelector("header > h3")?.TextContent;
//Get posts //Get posts
foreach (var article in section.QuerySelectorAll("article")) foreach (var article in section.QuerySelectorAll("article"))
{ {
var post = new PostInfo() var post = new PostInfo()
{ {
Body = article.QuerySelector("blockquote").InnerHtml, Body = article.QuerySelector("blockquote").InnerHtml,
}; };
(post.Name, post.Tripcode, post.Email, post.Capcode) = getTripcode(article.QuerySelector("header")); (post.Name, post.Tripcode, post.Email, post.Capcode) = getTripcode(article.QuerySelector("header"));
if (TryParseDateTime(article.QuerySelector("header > time").TextContent, out var _time)) if (TryParseDateTime(article.QuerySelector("header > time").TextContent, out var _time))
post.Timestamp = _time; post.Timestamp = _time;
else else
post.Timestamp = default; post.Timestamp = default;
if (ulong.TryParse(article.QuerySelector("header > nav > a[class=quote]").TextContent, out ulong _postNumber)) if (ulong.TryParse(article.QuerySelector("header > nav > a[class=quote]").TextContent, out ulong _postNumber))
post.PostNumber = _postNumber; post.PostNumber = _postNumber;
else else
post.PostNumber = default; post.PostNumber = default;
//Get modlog //Get modlog
getModlog(article.QuerySelector("b.modLog")?.InnerHtml, out var postModlog); getModlog(article.QuerySelector("b.modLog")?.InnerHtml, out var postModlog);
post.ModLog = postModlog; post.ModLog = postModlog;
var figure = article.QuerySelector("figure > figcaption > i"); var figure = article.QuerySelector("figure > figcaption > i");
if (figure != null) if (figure != null)
{ {
//Has image //Has image
if (TryParseImageDimInfo(figure.FirstChild.TextContent, out var _imageSize, out var _x, out var _y)) if (TryParseImageDimInfo(figure.FirstChild.TextContent, out var _imageSize, out var _x, out var _y))
{ {
post.ImageDimensions = (_x, _y); post.ImageDimensions = (_x, _y);
post.ImageSize = _imageSize; post.ImageSize = _imageSize;
post.ImageURL = figure.QuerySelector("a").GetAttribute("href"); post.ImageURL = figure.QuerySelector("a").GetAttribute("href");
post.ImageFilename = figure.QuerySelector("a").GetAttribute("download"); post.ImageFilename = figure.QuerySelector("a").GetAttribute("download");
} }
} }
await EncryptIfRequired(post, token); await EncryptIfRequired(post, token);
yield return post; yield return post;
} }
await EncryptIfRequired(thread, token); await EncryptIfRequired(thread, token);
} }
private async Task EncryptIfRequired(PostInfo post, CancellationToken token) private async Task EncryptIfRequired(PostInfo post, CancellationToken token)
{ {
try try
{ {
if (Config.EncryptDeleted != null) if (Config.EncryptDeleted != null)
{ {
if (post.ModLog.ImageDeleted) if (post.ModLog.ImageDeleted)
{ {
await post.EncryptImageAsync(Config.EncryptDeleted.Value, token); await post.EncryptImageAsync(Config.EncryptDeleted.Value, token);
} }
if (post.ModLog.PostDeleted) if (post.ModLog.PostDeleted)
{ {
await post.EncryptPostAsync(Config.EncryptDeleted.Value, token); await post.EncryptPostAsync(Config.EncryptDeleted.Value, token);
} }
} }
}catch(Exception ex) }catch(Exception ex)
{ {
Console.WriteLine("Encryption for post "+post.PostNumber+" failed: " + ex.Message+"\n"+ex.StackTrace); Console.WriteLine("Encryption for post "+post.PostNumber+" failed: " + ex.Message+"\n"+ex.StackTrace);
} }
} }
protected override async IAsyncEnumerable<ThreadInfo> GetThreads(BoardInfo boardInfo, [EnumeratorCancellation] CancellationToken token) protected override async IAsyncEnumerable<ThreadInfo> GetThreads(BoardInfo boardInfo, [EnumeratorCancellation] CancellationToken token)
{ {
var document = await context.OpenAsync(boardInfo.BoardURL + "catalog", token); var document = await context.OpenAsync(boardInfo.BoardURL + "catalog", token);
await GetBoardInfo(boardInfo, document, token); await GetBoardInfo(boardInfo, document, token);
var threadLinks = document.QuerySelectorAll("#catalog > article > a[class=history]"); var threadLinks = document.QuerySelectorAll("#catalog > article > a[class=history]");
foreach(var link in threadLinks) foreach(var link in threadLinks)
{ {
if (link.HasAttribute("href")) if (link.HasAttribute("href"))
{ {
var href = link.GetAttribute("href"); var href = link.GetAttribute("href");
if (ulong.TryParse(href, out ulong postNumber)) if (ulong.TryParse(href, out ulong postNumber))
{ {
yield return new ThreadInfo() yield return new ThreadInfo()
{ {
PostNumber = postNumber, PostNumber = postNumber,
}; };
} }
} }
} }
} }
} }
} }

@ -1,218 +1,218 @@
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading.Channels; using System.Threading.Channels;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Tools.Crypto; using Tools.Crypto;
namespace napdump namespace napdump
{ {
class Program class Program
{ {
public static readonly CancellationTokenSource globalCancel = new CancellationTokenSource(); public static readonly CancellationTokenSource globalCancel = new CancellationTokenSource();
public const string NineballBaseUrl = "https://nineball.party"; public const string NineballBaseUrl = "https://nineball.party";
public static readonly string[] NineballBoards = new[] { "/nap/", "/srsbsn/", "/staff/"}; public static readonly string[] NineballBoards = new[] { "/nap/", "/srsbsn/", "/staff/"};
static volatile int totalThreadsDownloaded = 0; static volatile int totalThreadsDownloaded = 0;
static volatile int totalBoardsDownloaded = 0; static volatile int totalBoardsDownloaded = 0;
static volatile int totalPostsDownloaded = 0; static volatile int totalPostsDownloaded = 0;
static async Task<BoardInfo> GrabBoard(Dumper dumper, string boardUrl, ChannelWriter<ThreadInfo> onNewThread, CancellationToken token) static async Task<BoardInfo> GrabBoard(Dumper dumper, string boardUrl, ChannelWriter<ThreadInfo> onNewThread, CancellationToken token)
{ {
TaskCompletionSource<BoardInfo> get = new TaskCompletionSource<BoardInfo>(); TaskCompletionSource<BoardInfo> get = new TaskCompletionSource<BoardInfo>();
Dumper.Hooks hooks = new Dumper.Hooks() Dumper.Hooks hooks = new Dumper.Hooks()
{ {
OnBoardRetrieved = (bi) => OnBoardRetrieved = (bi) =>
{ {
if (!token.IsCancellationRequested) if (!token.IsCancellationRequested)
get.SetResult(bi); get.SetResult(bi);
}, },
PrintDebug = false//boardUrl.EndsWith("srsbsn/"), PrintDebug = false//boardUrl.EndsWith("srsbsn/"),
}; };
using var _reg_get_cancel = token.Register(() => get.SetException(new OperationCanceledException())); using var _reg_get_cancel = token.Register(() => get.SetException(new OperationCanceledException()));
Console.WriteLine("\r [" + dumper.GetType().Name + "] Downloading " + boardUrl); Console.WriteLine("\r [" + dumper.GetType().Name + "] Downloading " + boardUrl);
try try
{ {
await foreach (var thread in dumper.Parse(boardUrl, hooks).WithCancellation(token)) await foreach (var thread in dumper.Parse(boardUrl, hooks).WithCancellation(token))
{ {
totalPostsDownloaded += thread.Children.Count + 1; totalPostsDownloaded += thread.Children.Count + 1;
totalThreadsDownloaded += 1; totalThreadsDownloaded += 1;
await onNewThread.WriteAsync(thread, token); await onNewThread.WriteAsync(thread, token);
} }
return await get.Task; return await get.Task;
} }
finally finally
{ {
Console.WriteLine("\r [" + dumper.GetType().Name + "] Complete " + boardUrl); Console.WriteLine("\r [" + dumper.GetType().Name + "] Complete " + boardUrl);
} }
} }
static async Task readOutputs(ChannelReader<ThreadInfo> reader, CancellationToken token) static async Task readOutputs(ChannelReader<ThreadInfo> reader, CancellationToken token)
{ {
Dictionary<string, int> numberPerBoard = new Dictionary<string, int>(); Dictionary<string, int> numberPerBoard = new Dictionary<string, int>();
await foreach(var thread in reader.ReadAllAsync(token)) await foreach(var thread in reader.ReadAllAsync(token))
{ {
string name = thread.BoardInfo.BoardName; string name = thread.BoardInfo.BoardName;
if (numberPerBoard.ContainsKey(name)) if (numberPerBoard.ContainsKey(name))
numberPerBoard[name] += 1; numberPerBoard[name] += 1;
else numberPerBoard.Add(name, 1); else numberPerBoard.Add(name, 1);
Console.Write($"\r"); Console.Write($"\r");
foreach(var kv in numberPerBoard) foreach(var kv in numberPerBoard)
{ {
Console.Write($"{kv.Key} - {kv.Value} "); Console.Write($"{kv.Key} - {kv.Value} ");
} }
} }
Console.WriteLine(); Console.WriteLine();
} }
static string getTimestamp(DateTime time) static string getTimestamp(DateTime time)
{ {
return $"{time.Year}-{time.Month}-{time.Day}-{time.Hour}.{time.Minute}.{time.Second}.{time.Millisecond}"; return $"{time.Year}-{time.Month}-{time.Day}-{time.Hour}.{time.Minute}.{time.Second}.{time.Millisecond}";
} }
static async Task DumpBoardInfo(BoardInfo bi) static async Task DumpBoardInfo(BoardInfo bi)
{ {
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); System.Runtime.Serialization.Formatters.Binary.BinaryFormatter binf = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
if (!Directory.Exists("dumps")) if (!Directory.Exists("dumps"))
Directory.CreateDirectory("dumps"); Directory.CreateDirectory("dumps");
var path = Path.Combine("dumps", $"{bi.SafeName ?? "unbound"}-{getTimestamp(DateTime.Now)}.board"); var path = Path.Combine("dumps", $"{bi.SafeName ?? "unbound"}-{getTimestamp(DateTime.Now)}.board");
using (var fs = new FileStream(path, FileMode.Create)) using (var fs = new FileStream(path, FileMode.Create))
{ {
await Task.Yield(); await Task.Yield();
binf.Serialize(fs, bi); binf.Serialize(fs, bi);
} }
Console.WriteLine($"\r {bi.BoardName} -> {path}"); Console.WriteLine($"\r {bi.BoardName} -> {path}");
} }
static async Task<DumperConfig> ParseArgs(string[] args) static async Task<DumperConfig> ParseArgs(string[] args)
{ {
string findarg(string name) string findarg(string name)
{ {
for(int i=0;i<args.Length-1;i++) for(int i=0;i<args.Length-1;i++)
{ {
if (args[i].ToLower() == name) if (args[i].ToLower() == name)
{ {
var r = args[i + 1]; var r = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray(); args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
return r; return r;
} }
} }
return null; return null;
} }
bool tryarg<T>(string name, TryParser<T> parser, out T value, T def = default) bool tryarg<T>(string name, TryParser<T> parser, out T value, T def = default)
{ {
var fa = findarg(name); var fa = findarg(name);
if(fa!=null) if(fa!=null)
{ {
if(! parser(fa, out value)) if(! parser(fa, out value))
{ {
value = def; value = def;
return false; return false;
} }
return true; return true;
} }
value = def; value = def;
return false; return false;
} }
static bool defp(string inp, out string op) static bool defp(string inp, out string op)
{ {
op = inp; op = inp;
return true; return true;
} }
string login; string login;
string aesKeyfile; string aesKeyfile;
int threads; int threads;
tryarg("--login", defp, out login); tryarg("--login", defp, out login);
tryarg<int>("--threads", int.TryParse, out threads, 3); tryarg<int>("--threads", int.TryParse, out threads, 3);
tryarg("--encrypt-deleted", defp, out aesKeyfile); tryarg("--encrypt-deleted", defp, out aesKeyfile);
return new DumperConfig(threads, Cookies: login == null ? null : new[] { (NineballBaseUrl, "a=" + login) }, EncryptDeleted: aesKeyfile == null ? (AESKey?)null : (await getKey(aesKeyfile))); return new DumperConfig(threads, Cookies: login == null ? null : new[] { (NineballBaseUrl, "a=" + login) }, EncryptDeleted: aesKeyfile == null ? (AESKey?)null : (await getKey(aesKeyfile)));
} }
private static async Task<AESKey> getKey(string fn) private static async Task<AESKey> getKey(string fn)
{ {
using (var fs = new FileStream(fn, FileMode.Open, FileAccess.Read)) using (var fs = new FileStream(fn, FileMode.Open, FileAccess.Read))
{ {
return await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Keyfile is password protected: ")); return await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Keyfile is password protected: "));
} }
} }
private delegate bool TryParser<T>(string input, out T value); private delegate bool TryParser<T>(string input, out T value);
static async Task Main(string[] args) static async Task Main(string[] args)
{ {
if(args.Length==1 && args[0].ToLower()=="--help") if(args.Length==1 && args[0].ToLower()=="--help")
{ {
Console.WriteLine("napdump.exe [--login <login token `a'>] [--threads <concurrent downloader number>] [--encrypt-deleted <deleted key>]"); Console.WriteLine("napdump.exe [--login <login token `a'>] [--threads <concurrent downloader number>] [--encrypt-deleted <deleted key>]");
return; return;
} }
using var napDownloader = new Dumpers.Nineball(await ParseArgs(args)); using var napDownloader = new Dumpers.Nineball(await ParseArgs(args));
Console.CancelKeyPress += (o,e) => Console.CancelKeyPress += (o,e) =>
{ {
globalCancel.Cancel(); globalCancel.Cancel();
e.Cancel = true; e.Cancel = true;
}; };
List<Task<BoardInfo>> downloaders = new List<Task<BoardInfo>>(); List<Task<BoardInfo>> downloaders = new List<Task<BoardInfo>>();
Channel<ThreadInfo> threads = Channel.CreateUnbounded<ThreadInfo>(); Channel<ThreadInfo> threads = Channel.CreateUnbounded<ThreadInfo>();
Task outputReader = readOutputs(threads.Reader, globalCancel.Token); Task outputReader = readOutputs(threads.Reader, globalCancel.Token);
try try
{ {
foreach (var board in NineballBoards) foreach (var board in NineballBoards)
{ {
downloaders.Add(Task.Run(async () => downloaders.Add(Task.Run(async () =>
{ {
try try
{ {
var bi = await GrabBoard(napDownloader, NineballBaseUrl + board, threads.Writer, globalCancel.Token); var bi = await GrabBoard(napDownloader, NineballBaseUrl + board, threads.Writer, globalCancel.Token);
totalBoardsDownloaded += 1; totalBoardsDownloaded += 1;
try try
{ {
await DumpBoardInfo(bi); await DumpBoardInfo(bi);
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("Failed to dump board " + bi.BoardName + " to file: " + ex.Message); Console.WriteLine("Failed to dump board " + bi.BoardName + " to file: " + ex.Message);
} }
return bi; return bi;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("Failed to download board " + board + ": " + ex.Message); Console.WriteLine("Failed to download board " + board + ": " + ex.Message);
return default(BoardInfo); return default(BoardInfo);
} }
})); }));
} }
var boards = (await Task.WhenAll(downloaders)).Where(x => x != null); var boards = (await Task.WhenAll(downloaders)).Where(x => x != null);
Console.WriteLine("\n\nDownloaded Boards:"); Console.WriteLine("\n\nDownloaded Boards:");
foreach (var b in boards) foreach (var b in boards)
Console.WriteLine("\t" + b.ToString()); Console.WriteLine("\t" + b.ToString());
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("\n\nError: " + ex.Message); Console.WriteLine("\n\nError: " + ex.Message);
return; return;
} }
finally finally
{ {
threads.Writer.Complete(); threads.Writer.Complete();
try try
{ {
await outputReader; await outputReader;
} }
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
} }
Console.WriteLine("Complete"); Console.WriteLine("Complete");
Console.WriteLine($"Downloaded {totalBoardsDownloaded} boards, with {totalThreadsDownloaded} threads, containing {totalPostsDownloaded} posts."); Console.WriteLine($"Downloaded {totalBoardsDownloaded} boards, with {totalThreadsDownloaded} threads, containing {totalPostsDownloaded} posts.");
} }
} }
} }

@ -126,10 +126,11 @@ namespace ndview
if (post.ImageURL != null) if (post.ImageURL != null)
await WriteImageFigure(index, post, token); await WriteImageFigure(index, post, token);
await using (await index.TagAsync("blockquote", token)) await WriteBody(index, post, token);
/*await using (await index.TagAsync("blockquote", token))
{ {
await index.AppendHtml(post.Body); await index.AppendHtml(post.Body);
} }*/
} }
await imageExtractor; await imageExtractor;
@ -222,6 +223,11 @@ namespace ndview
{ {
await index.AppendHtml(post.Body); await index.AppendHtml(post.Body);
} }
/*if(post.ModLog.UserBanned)
{
await index.AppendHtml("<div class='ban'>USER WAS BANNED FOR THIS POST</div>");
}*/ //TODO: Idk?
} }
private async Task WriteThread(HtmlGenerator index, ThreadInfo thread, DirectoryInfo img, bool wasEnc, CancellationToken token) private async Task WriteThread(HtmlGenerator index, ThreadInfo thread, DirectoryInfo img, bool wasEnc, CancellationToken token)
@ -327,7 +333,7 @@ namespace ndview
} }
await using (await index.TagAsync("div", cancel, ("class", "stat"))) await using (await index.TagAsync("div", cancel, ("class", "stat")))
{ {
await index.Append($"Showing {Board.Threads.Count} threads containing {Board.Threads.Select(x => x.Children.Count).Sum()} posts and {Board.Threads.Count + Board.Threads.Select(x => x.Children.Where(y => y.ImageURL != null).Count()).Sum()} images.", cancel); await index.Append($"Showing {Board.Threads.Count} threads containing {Board.Threads.Select(x => x.Children.Count).Sum()} posts and {Board.Threads.Count + Board.Threads.Select(x => x.Children.Where(y => y.IsImageEncrypted || y.ImageURL != null).Count()).Sum()} images.", cancel);
await index.AppendHtml($"<br />Taken at <time>{Board.DumpTimestamp.ToString()}</time>.<br />Original: <a href='{Board.BoardURL}'>", cancel); await index.AppendHtml($"<br />Taken at <time>{Board.DumpTimestamp.ToString()}</time>.<br />Original: <a href='{Board.BoardURL}'>", cancel);
await index.Append(Board.BoardName, cancel); await index.Append(Board.BoardName, cancel);
await index.AppendHtml($"</a>", cancel); await index.AppendHtml($"</a>", cancel);
@ -340,6 +346,13 @@ namespace ndview
{ {
await using(await index.TagAsync("nav", cancel, ("class", "script"))) await using(await index.TagAsync("nav", cancel, ("class", "script")))
{ {
await using(await index.TagAsync("span", cancel)) {
await using(await index.TagAsync("a", cancel, ("href", Board.BoardURL)))
await index.Append(Board.BoardName);
await using(await index.TagAsync("time"))
await index.Append(Board.DumpTimestamp.ToString());
}
await using(await index.TagAsync("ul", cancel)) { await using(await index.TagAsync("ul", cancel)) {
await using(await index.TagAsync("li", cancel)) await using(await index.TagAsync("li", cancel))
await using(await index.TagAsync("a", cancel, ("href", "#!"), ("id", "expand_all_threads"))) await using(await index.TagAsync("a", cancel, ("href", "#!"), ("id", "expand_all_threads")))

@ -1,302 +1,304 @@
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Threading; using System.Threading;
using System.Linq; using System.Linq;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization.Formatters.Binary;
using System.Security; using System.Security;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Collections.Generic; using System.Collections.Generic;
using Tools.Crypto; using Tools.Crypto;
namespace ndview namespace ndview
{ {
class Program class Program
{ {
static void Usage() static void Usage()
{ {
Console.WriteLine("ndview [--deleted-key <deleted key>] <archive> [<key file>] <output directory>"); Console.WriteLine("ndview [--deleted-key <deleted key>] <archive> [<key file>] <output directory>");
} }
static async Task<TempFile> Decrypt(Stream from, Stream keyFile, Func<string> getPasssword, CancellationToken token) static async Task<TempFile> Decrypt(Stream from, Stream keyFile, Func<string> getPasssword, CancellationToken token)
{ {
using (var enc = new encaes.AesEncryptor(from) { KeepAlive = true }) using (var enc = new encaes.AesEncryptor(from) { KeepAlive = true })
{ {
enc.Key = await encaes.AesEncryptor.LoadKey(keyFile, getPasssword, token); enc.Key = await encaes.AesEncryptor.LoadKey(keyFile, getPasssword, token);
var tempFile = new TempFile(); var tempFile = new TempFile();
try try
{ {
await enc.Decrypt(tempFile.Stream, token); await enc.Decrypt(tempFile.Stream, token);
return tempFile; return tempFile;
} }
catch (Exception ex) catch (Exception ex)
{ {
await tempFile.DisposeAsync(); await tempFile.DisposeAsync();
throw ex; throw ex;
} }
} }
} }
static async Task<TempDirectory> Extract(Stream from, CancellationToken token) static async Task<TempDirectory> Extract(Stream from, CancellationToken token)
{ {
var td = new TempDirectory(); var td = new TempDirectory();
try try
{ {
FastZip zip = new FastZip(); FastZip zip = new FastZip();
await Task.Yield(); await Task.Yield();
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
zip.ExtractZip(from, td.Directory.FullName, FastZip.Overwrite.Always, (x) => true, null, null, false, false); zip.ExtractZip(from, td.Directory.FullName, FastZip.Overwrite.Always, (x) => true, null, null, false, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
td.Dispose(); td.Dispose();
throw ex; throw ex;
} }
return td; return td;
} }
static (FileInfo BoardFile, DirectoryInfo ImagesDirectory) select(DirectoryInfo folder) static (FileInfo BoardFile, DirectoryInfo ImagesDirectory) select(DirectoryInfo folder)
{ {
FileInfo bi = null; FileInfo bi = null;
DirectoryInfo di = null; DirectoryInfo di = null;
foreach (var fi in folder.GetFiles()) foreach (var fi in folder.GetFiles())
{ {
if (fi.Name.EndsWith(".board")) if (fi.Name.EndsWith(".board"))
{ {
string nnm = string.Join('.', fi.Name.Split(".")[..^1]); string nnm = string.Join('.', fi.Name.Split(".")[..^1]);
if (Directory.Exists(Path.Combine(folder.FullName, nnm))) if (Directory.Exists(Path.Combine(folder.FullName, nnm)))
{ {
return (fi, new DirectoryInfo(Path.Combine(folder.FullName, nnm))); return (fi, new DirectoryInfo(Path.Combine(folder.FullName, nnm)));
} }
} }
bi = fi; bi = fi;
} }
if (bi == null) throw new InvalidDataException("No board info found."); if (bi == null) throw new InvalidDataException("No board info found.");
di = folder.GetDirectories().FirstOrDefault(); di = folder.GetDirectories().FirstOrDefault();
if (di == null) throw new InvalidDataException("No images dir found"); if (di == null) throw new InvalidDataException("No images dir found");
return (bi, di); return (bi, di);
} }
static async Task generate(Stream bif, DirectoryInfo images, DirectoryInfo output, CancellationToken token) static async Task generate(Stream bif, DirectoryInfo images, DirectoryInfo output, CancellationToken token)
{ {
var binf = new BinaryFormatter(); var binf = new BinaryFormatter();
await Task.Yield(); await Task.Yield();
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var bi = (napdump.BoardInfo)binf.Deserialize(bif); var bi = (napdump.BoardInfo)binf.Deserialize(bif);
if (!output.Exists) output.Create(); if (!output.Exists) output.Create();
var gen = new PageGenerator(bi, images); var gen = new PageGenerator(bi, images);
Console.WriteLine($" starting {bi.BoardName} w/ {images.Name} -> {output.FullName}"); Console.WriteLine($" starting {bi.BoardName} w/ {images.Name} -> {output.FullName}");
await gen.GenerateFull(output, token); await gen.GenerateFull(output, token);
} }
static Task<string> ReadPassword(string prompt = null, CancellationToken token = default) static Task<string> ReadPassword(string prompt = null, CancellationToken token = default)
{ {
TaskCompletionSource<string> comp = new TaskCompletionSource<string>(); TaskCompletionSource<string> comp = new TaskCompletionSource<string>();
List<char> pwd = new List<char>(); List<char> pwd = new List<char>();
bool set = false; bool set = false;
_ = Task.Run(() => _ = Task.Run(() =>
{ {
if (prompt != null) if (prompt != null)
Console.Write(prompt); Console.Write(prompt);
try try
{ {
using var _c = token.Register(() => using var _c = token.Register(() =>
{ {
if (!set) if (!set)
{ {
set = true; set = true;
comp.SetException(new OperationCanceledException()); comp.SetException(new OperationCanceledException());
} }
}); });
while (true) while (true)
{ {
ConsoleKeyInfo i = Console.ReadKey(true); ConsoleKeyInfo i = Console.ReadKey(true);
if (token.IsCancellationRequested) break; if (token.IsCancellationRequested) break;
if (i.Key == ConsoleKey.Enter) if (i.Key == ConsoleKey.Enter)
{ {
Console.WriteLine(); Console.WriteLine();
break; break;
} }
else if (i.Key == ConsoleKey.Backspace) else if (i.Key == ConsoleKey.Backspace)
{ {
if (pwd.Count > 0) if (pwd.Count > 0)
{ {
pwd.RemoveAt(pwd.Count - 1); pwd.RemoveAt(pwd.Count - 1);
} }
} }
else if (i.KeyChar != '\u0000') else if (i.KeyChar != '\u0000')
{ {
pwd.Add(i.KeyChar); pwd.Add(i.KeyChar);
} }
} }
if (!set) if (!set)
{ {
set = true; set = true;
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
comp.SetException(new OperationCanceledException()); comp.SetException(new OperationCanceledException());
else else
comp.SetResult(new string(pwd.ToArray())); comp.SetResult(new string(pwd.ToArray()));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine(); Console.WriteLine();
pwd.Clear(); pwd.Clear();
if (!set) if (!set)
{ {
set = true; set = true;
comp.SetException(ex); comp.SetException(ex);
} }
} }
}); });
return comp.Task; return comp.Task;
} }
static readonly CancellationTokenSource cancel = new CancellationTokenSource(); static readonly CancellationTokenSource cancel = new CancellationTokenSource();
public static AESKey? DeletedKey { get; private set; } = null; public static AESKey? DeletedKey { get; private set; } = null;
static async Task loadAesKey(string from) static async Task loadAesKey(string from)
{ {
using (var fs = new FileStream(from, FileMode.Open, FileAccess.Read)) using (var fs = new FileStream(from, FileMode.Open, FileAccess.Read))
{ {
DeletedKey = await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Key is password protected: ")); DeletedKey = await encaes.AesEncryptor.LoadKey(fs, async () => await encaes.AesEncryptor.ReadPassword("Key is password protected: "));
} }
} }
static Task parseArgs(ref string[] args) static Task parseArgs(ref string[] args)
{ {
string delk = null; string delk = null;
for(int i=0;i<args.Length-1;i++) for(int i=0;i<args.Length-1;i++)
{ {
if(args[i].ToLower() == "--deleted-key") if(args[i].ToLower() == "--deleted-key")
{ {
delk = args[i + 1]; delk = args[i + 1];
args = args.Where((_, j) => j != i && j != (i + 1)).ToArray(); args = args.Where((_, j) => j != i && j != (i + 1)).ToArray();
} Console.WriteLine("Set deleted key to "+delk);
} }
}
if (delk == null) return Task.CompletedTask;
else if (delk == null) return Task.CompletedTask;
{ else
return loadAesKey(delk); {
} return loadAesKey(delk);
} }
static async Task Main(string[] args) }
{ static async Task Main(string[] args)
if (args.Length < 2) {
{ if (args.Length < 2)
Usage(); {
return; Usage();
} return;
else }
{ else
try {
{ try
await parseArgs(ref args); {
} await parseArgs(ref args);
catch (Exception ex) }
{ catch (Exception ex)
Console.WriteLine("Failed to load deleted key: " + ex.Message); {
return; Console.WriteLine("Failed to load deleted key: " + ex.Message);
} return;
if (File.Exists(args[0])) }
{ if (File.Exists(args[0]))
Console.CancelKeyPress += (o, e) => {
{ Console.CancelKeyPress += (o, e) =>
if (!cancel.IsCancellationRequested) {
{ if (!cancel.IsCancellationRequested)
cancel.Cancel(); {
e.Cancel = true; cancel.Cancel();
} e.Cancel = true;
}; }
try };
{ try
if (await encaes.AesEncryptor.IsEncryptedFile(args[0], cancel.Token)) {
{ if (await encaes.AesEncryptor.IsEncryptedFile(args[0], cancel.Token))
//decrypt & extract {
if (args.Length < 3) { Usage(); return; } //decrypt & extract
if (args.Length < 3) { Usage(); return; }
TempFile tf = null;
try TempFile tf = null;
{ try
Console.WriteLine("Decrypting..."); {
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read)) Console.WriteLine("Decrypting...");
{ using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
using (var keyStream = new FileStream(args[1], FileMode.Open, FileAccess.Read)) {
{ using (var keyStream = new FileStream(args[1], FileMode.Open, FileAccess.Read))
tf = await Decrypt(inputFile, keyStream, () => {
{ tf = await Decrypt(inputFile, keyStream, () =>
var tsk = ReadPassword("Password: ", cancel.Token); {
tsk.Wait(); var tsk = ReadPassword("Password: ", cancel.Token);
cancel.Token.ThrowIfCancellationRequested(); tsk.Wait();
if (tsk.IsFaulted) cancel.Token.ThrowIfCancellationRequested();
throw tsk.Exception; if (tsk.IsFaulted)
return tsk.Result; throw tsk.Exception;
}, cancel.Token); return tsk.Result;
} }, cancel.Token);
} }
}
tf.Stream.Position = 0;
Console.WriteLine("Extracting..."); tf.Stream.Position = 0;
using (var tempd = await Extract(tf.Stream, cancel.Token)) Console.WriteLine("Extracting...");
{ using (var tempd = await Extract(tf.Stream, cancel.Token))
Console.WriteLine("Selecting best matches"); {
var (boardFile, imagesDir) = select(tempd.Directory); Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read)) Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
await generate(fs, imagesDir, new DirectoryInfo(args[2]), cancel.Token); using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
Console.WriteLine("Complete"); await generate(fs, imagesDir, new DirectoryInfo(args[2]), cancel.Token);
} Console.WriteLine("Complete");
}
}
finally }
{ finally
tf?.Dispose(); {
} tf?.Dispose();
} }
else }
{ else
//extract {
TempDirectory tempd; //extract
using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read)) TempDirectory tempd;
{ using (var inputFile = new FileStream(args[0], FileMode.Open, FileAccess.Read))
Console.WriteLine("Extracting..."); {
tempd = await Extract(inputFile, cancel.Token); Console.WriteLine("Extracting...");
} tempd = await Extract(inputFile, cancel.Token);
try }
{ try
Console.WriteLine("Selecting best matches"); {
var (boardFile, imagesDir) = select(tempd.Directory); Console.WriteLine("Selecting best matches");
var (boardFile, imagesDir) = select(tempd.Directory);
Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read)) Console.WriteLine($"Begining generate for {boardFile.Name} with {imagesDir.Name}");
await generate(fs, imagesDir, new DirectoryInfo(args[1]), cancel.Token); using (var fs = new FileStream(boardFile.FullName, FileMode.Open, FileAccess.Read))
Console.WriteLine("Complete"); await generate(fs, imagesDir, new DirectoryInfo(args[1]), cancel.Token);
} Console.WriteLine("Complete");
finally }
{ finally
tempd.Dispose(); {
} tempd.Dispose();
} }
} }
catch (OperationCanceledException) { } }
catch (Exception ex) catch (OperationCanceledException) { }
{ catch (Exception ex)
Console.WriteLine($"Error ({ex.GetType().Name}) in generation: {ex.Message}"); {
} Console.WriteLine($"Error ({ex.GetType().Name}) in generation: {ex.Message}");
} Console.WriteLine(ex.StackTrace);
else Console.WriteLine("Input archive must exist."); }
} }
cancel.Dispose(); else Console.WriteLine("Input archive must exist.");
}
} cancel.Dispose();
}
} }
}
}

@ -119,13 +119,25 @@ a:hover {
nav { nav {
position: fixed; position: fixed;
float: right; right: 0;
right: 10px; top: 0;
top: 10px; width: 100%;
background-color: white; background: rgba(214,218,240,.7);
padding: 5px; border-bottom: 1px solid #b7c5d9;
border: solid 1px;
border-color: black; }
nav > span {
display:inline-block;
padding-left: 10px;
}
nav ul {
display: flex;
justify-content: center;
list-style-type: none;
margin: 0;
padding: 0;
float:right;
} }
nav :not(a) nav :not(a)
@ -141,7 +153,7 @@ nav ul {
padding: 0; padding: 0;
} }
nav ul::before { nav ul::before {
content: "< "; content: "[ ";
} }
nav ul li { nav ul li {
padding: 0 2px; padding: 0 2px;
@ -150,5 +162,32 @@ nav ul li:not(:last-child)::after {
content: " | "; content: " | ";
} }
nav ul::after { nav ul::after {
content: " >"; content: " ]";
}
nav > span > time {
padding-left: 10px;
padding-right: 10px;
}
article > header {
margin-left: 10px;
padding-top: 1px;
}
a, a:visited {
color: blue;
}
@media only screen and (max-width: 792px)
{
nav > span {
display:none!important;
}
}
@media only screen and (max-width: 542px)
{
nav {
display:none!important;
}
} }

Loading…
Cancel
Save