package the8472.mldht.cli;
import static the8472.utils.Functional.typedGet;
import the8472.bencode.PrettyPrinter;
import the8472.bencode.Tokenizer.BDecodingException;
import the8472.bt.TorrentUtils;
import the8472.utils.concurrent.SerializedTaskExecutor;
import lbms.plugins.mldht.kad.Key;
import lbms.plugins.mldht.kad.utils.ThreadLocalUtils;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class TorrentInfo {
Path source;
ByteBuffer raw;
Map<String, Object> root;
Map<String, Object> info;
Charset encoding = StandardCharsets.UTF_8;
public TorrentInfo(Path source) {
this.source = source;
}
void readRaw() {
if(raw != null)
return;
try(FileChannel chan = FileChannel.open(source, StandardOpenOption.READ)) {
raw = chan.map(MapMode.READ_ONLY, 0, chan.size());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
void decode() {
if(root != null)
return;
readRaw();
root = ThreadLocalUtils.getDecoder().decode(raw.duplicate());
typedGet(root, "info", Map.class).ifPresent(i -> info = i);
if(info != null) {
String charset = typedGet(info, "encoding", byte[].class).map(b -> new String(b, StandardCharsets.ISO_8859_1)).orElse(null);
if(charset != null) {
try {
this.encoding = Charset.forName(charset);
} catch (Exception e) {
System.err.println("Charset " + charset + "not supported, falling back to " + encoding.name());
}
}
}
}
Key infoHash() {
return TorrentUtils.infohash(raw);
}
Optional<String> name() {
decode();
Optional<String> name = typedGet(info, "name.utf-8", byte[].class).map(b -> new String(b, StandardCharsets.UTF_8));
if(!name.isPresent()) {
name = typedGet(info, "name", byte[].class).map(b -> new String(b, encoding));
}
return name;
}
List<Map<String, Object>> files() {
return typedGet(info, "files", List.class).map((List l) -> {
return (List<Map<String, Object>>)l.stream().filter(Map.class::isInstance).collect(Collectors.toList());
}).orElse(Collections.emptyList());
}
String raw() {
decode();
PrettyPrinter p = new PrettyPrinter();
p.indent(" ");
p.guessHumanReadableStringValues(true);
p.append(root);
return p.toString();
}
public static void main(String[] argsAry) throws IOException, InterruptedException {
List<String> args = new ArrayList<>(Arrays.asList(argsAry));
boolean printRaw = ParseArgs.extractBool(args, "-raw");
boolean recursive = ParseArgs.extractBool(args, "-r");
boolean printLargest = ParseArgs.extractBool(args, "-largest");
Stream<Path> torrents = args.parallelStream().unordered().map(Paths::get).filter(Files::exists).flatMap(p -> {
try {
return Files.find(p, recursive ? Integer.MAX_VALUE : 1 , (f, attr) -> {
return attr.isRegularFile() && attr.size() > 0;
}, FileVisitOption.FOLLOW_LINKS);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}); //.collect(Collectors.toSet()).parallelStream().unordered();
Consumer<String> printer = SerializedTaskExecutor.runSerialized((String s) -> {
System.out.println(s);
});
String newline = "\\u000a|\\u000b|\\u000c|\\u000d|\\u0085|\\u2028|\\u2029";
torrents.map(p -> {
TorrentInfo ti = new TorrentInfo(p);
try {
ti.decode();
} catch(BDecodingException ex) {
return p.toString() + " does not appear to be a bencoded file: " + ex.getMessage();
}
if(printRaw)
return p.toString() + "\n" + ti.raw() + '\n';
if(ti.info == null)
return p.toString() + " does not contain an info dictionary";
long length = typedGet(ti.info, "length", Long.class).orElse(0L);
long largestSize = length;
int numFiles = 1;
StringBuilder result = new StringBuilder();
Optional<String> name = ti.name();
if(!name.isPresent()) {
return p.toString() + " does not contain a name field";
}
String largestFile = "";
List<Map<String, Object>> files = ti.files();
if(!files.isEmpty()) {
length = files.stream().mapToLong(e -> typedGet(e, "length", Long.class).orElse(0L)).sum();
numFiles = files.size();
Map<String, Object> largest = files.stream().max(Comparator.comparing(e -> typedGet(e, "length", Long.class).orElse(0L))).get();
largestSize = typedGet(largest, "length", Long.class).orElse(0L);
List<?> path = typedGet(largest, "path.utf-8", List.class).orElse(null);
if(path == null)
path = typedGet(largest, "path", List.class).orElse(null);
largestFile = path.stream().filter(byte[].class::isInstance).map(b -> new String((byte[]) b, StandardCharsets.UTF_8)).collect(Collectors.joining("/"));
largestFile = largestFile.replaceAll(newline, " ");
}
result.append(p.toString());
result.append(" ");
ti.name().map(s -> s.replaceAll(newline, " ")).ifPresent(result::append);
if(printLargest) {
if(numFiles > 1) {
result.append('/');
result.append(largestFile);
}
result.append(" size:");
result.append(largestSize);
result.append('/');
result.append(length);
result.append(" files:");
result.append(numFiles);
} else {
result.append(" size:");
result.append(length);
result.append(" files:");
result.append(numFiles);
}
return result.toString();
}).forEach(printer::accept);
}
}