Added mediainfo script

This commit is contained in:
DataHoarder 2021-11-21 04:17:10 +01:00
parent 4564536326
commit c656e63fe8
2 changed files with 868 additions and 1 deletions

View file

@ -0,0 +1,867 @@
// ==UserScript==
// @name AnimeBytes Mediainfo Improvements
// @author WeebDataHoarder
// @version 1.0
// @downloadURL https://git.gammaspectra.live/WeebDataHoarder/userscripts/raw/branch/master/AnimeBytes/ab-mediainfo.user.js
// @updateURL https://git.gammaspectra.live/WeebDataHoarder/userscripts/raw/branch/master/AnimeBytes/ab-mediainfo.user.js
// @description AnimeBytes Mediainfo Improvements. Adds several listing and matching releases against mediainfo utilities. MIT license
// @match https://animebytes.tv/torrents.php?*id=*
// @icon http://animebytes.tv/favicon.ico
// @run-at document-end
// ==/UserScript==
function parseMediaInfo(text){
let ob = {};
let currentKey = null;
let currentOb = null;
try{
text.split("\n").forEach((line) => {
const cleanLine = line.trim();
if(cleanLine === ""){
return;
}else if(!cleanLine.includes(":")){
if(currentOb !== null && Object.keys(currentOb).length === 0){
//Nothing added, error, invalid mediainfo
throw ("Invalid key " + currentKey);
}
currentKey = cleanLine.toLowerCase();
let listMatch = currentKey.match(/^(?<kind>(video|audio|text))( #)?(?<number>[0-9]*)$/i);
currentOb = {};
if(listMatch !== null){
let k = listMatch.groups.kind;
if(!(k in ob)){
ob[k] = [];
}
ob[k].push(currentOb);
}else{
ob[currentKey] = currentOb;
}
}else if(currentOb !== null){
const matches = cleanLine.match(/^(?<key>((?!\s:).)+)[\s]+:(?<value>.+)$/);
if(matches !== null){
let key = matches.groups.key.trim().toLowerCase().replace(/[ \/]/g, "_").replace(/[(),]/g, "");
currentOb[key] = matches.groups.value.trim();
}else{
throw "Invalid entry";
}
}else{
throw "Invalid state";
}
});
}catch (e){
console.log(text);
console.log(e);
return null;
}
return ob;
}
function getLineTagEntry(v, legacy){
if(typeof v === 'object'){
return legacy ? v.legacy : (v.name/* + ("lossless" in v ? " (Lossless)" : "")*/);
}
return v;
}
function getAudioChannels(a){
if(a === 3){
return "2.1";
}else if(a === 5){
return "4.1";
}else if(a === 6){
return "5.1";
}else if(a === 8){
return "7.1";
}
return a + ".0";
}
function getComparisonLine(tags){
let entries = [];
entries.push(getLineTagEntry(tags.source, true));
entries.push(getLineTagEntry(tags.container, true) + ("region" in tags ? " (" + tags.region + ")" : ""));
if("aspectRatio" in tags){
entries.push(tags.aspectRatio);
}
if("videoCodec" in tags){
entries.push(getLineTagEntry(tags.videoCodec, true));
}
entries.push(tags.resolution);
entries.push(getLineTagEntry(tags.audioCodec, true) + " " + getAudioChannels(tags.audioChannels));
if(tags.audioCount > 1){
entries.push("Dual Audio");
}
entries.push(("subtitleType" in tags ? getLineTagEntry(tags.subtitleType, true) : "RAW") + ("group" in tags ? " (" + tags.group + ")" : ""));
return entries.join(" | ");
}
function getEntryLine(tags){
let entries = [];
entries.push(getLineTagEntry(tags.source, false) + ("sourceName" in tags ? " (" + tags.sourceName + ")" : ""));
entries.push(getLineTagEntry(tags.container, false) + ("region" in tags ? " (" + tags.region + ")" : ""));
entries.push(getLineTagEntry(tags.videoCodec, false) + ("videoEncoder" in tags ? " (" + tags.videoEncoder + ("videoCRF" in tags ? " " + tags.videoCRF : "") + ")" : ""));
if("videoHDR" in tags){
entries.push(getLineTagEntry(tags.videoHDR, false));
}
entries.push(tags.resolution);
entries.push(getLineTagEntry(tags.audioCodec, false) + " " + getAudioChannels(tags.audioChannels));
if(tags.audioCount > 1){
entries.push("Dual Audio");
}
entries.push(("subtitleCodec" in tags ? tags.subtitleCodec + " " : "") + ("subtitleType" in tags ? getLineTagEntry(tags.subtitleType, false) : "RAW") + ("group" in tags ? " (" + tags.group + ")" : ""));
return entries.join(" | ");
}
const allowedVideoTypes = [
"Movie",
"OVA",
"ONA",
"TV Series",
"TV Special",
"DVD Special",
"BD Special"
];
const knownSources = [
"TV",
"DVD",
"DVD5",
"DVD9",
"Blu-ray",
"UHD Blu-ray",
"HD DVD",
"VHS",
"LD",
"Web"
];
const knownContainers = [
"AVI",
"MKV",
"MP4",
"OGM",
"WMV",
"MPG",
"ISO",
"VOB",
"VOB IFO",
"TS",
"M2TS",
"FLV",
"RMVB"
];
const knownVideoCodecs = [
"h264",
"h264 10-bit",
"h265",
"h265 10-bit",
"XviD",
"DivX",
"WMV",
"MPEG-1/2",
"VC-1",
"MPEG-TS",
"DVD5",
"DVD9",
"RealVideo",
"VP6",
"VP9",
"AV1"
];
const knownAudioCodecs = [
"MP3",
"Vorbis",
"Opus",
"AAC",
"AC3",
"TrueHD",
"DTS",
"DTS-ES",
"FLAC",
"PCM",
"WMA",
"MP2",
"WAV",
"DTS-HD",
"DTS-HD MA",
"RealAudio"
];
const audioCodecs = [
{
name: "E-AC3",
match: {
codec_id: "A_EAC3",
number_of_dynamic_objects: null
},
legacy: "AC3"
},
{
name: "E-AC3 Atmos",
match: {
codec_id: "A_EAC3",
number_of_dynamic_objects: /^[0-9]+$/
},
legacy: "AC3"
},
{
name: "TrueHD Atmos",
match: {
codec_id: "A_TRUEHD",
number_of_dynamic_objects: /^[0-9]+$/
},
legacy: "TrueHD",
lossless: true
},
{
name: "DTS:X MA",
match: {
codec_id: "A_DTS",
format: "DTS XLL X",
channels_original: "Object Based"
},
legacy: "DTS-HD MA",
lossless: true
},
{
name: "DTS-HD MA",
match: {
codec_id: "A_DTS",
format: "DTS XLL",
channels_original: null,
},
legacy: "DTS-HD MA",
lossless: true
}
];
const videoCodecs = [
{
name: "h264 10-bit",
match: {
codec_id: "V_MPEG4/ISO/AVC",
bit_depth: "10 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "h264 10-bit"
},
{
name: "h264 10-bit 4:4:4",
match: {
codec_id: "V_MPEG4/ISO/AVC",
bit_depth: "10 bits",
chroma_subsampling: /^4:4:4/
},
legacy: "h264 10-bit"
},
{
name: "h264",
match: {
codec_id: "V_MPEG4/ISO/AVC",
bit_depth: "8 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "h264"
},
{
name: "AV1",
match: {
codec_id: "V_AV1",
bit_depth: "8 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "AV1"
},
{
name: "AV1 10-bit",
match: {
codec_id: "V_AV1",
bit_depth: "10 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "AV1"
},
{
name: "h265 12-bit",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "12 bits",
chroma_subsampling: /^4:2:0/,
},
legacy: "h265 12-bit"
},
{
name: "h265 12-bit 4:2:0",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "12 bits",
chroma_subsampling: /^4:2:2/,
},
legacy: "h265 12-bit"
},
{
name: "h265 12-bit 4:4:4",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "12 bits",
chroma_subsampling: /^4:4:4/,
},
legacy: "h265 12-bit"
},
{
name: "h265 10-bit",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "10 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "h265 10-bit"
},
{
name: "h265 10-bit 4:2:@",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "10 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "h265 10-bit"
},
{
name: "h265 10-bit 4:4:4",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "10 bits",
chroma_subsampling: /^4:4:4/
},
legacy: "h265 10-bit"
},
{
name: "h265",
match: {
codec_id: "V_MPEGH/ISO/HEVC",
bit_depth: "8 bits",
chroma_subsampling: /^4:2:0/
},
legacy: "h265"
},
];
const torrentType = document.querySelector("a.scaledImg > img").getAttribute("title");
if(allowedVideoTypes.indexOf(torrentType.replace("Live Action ", "")) !== -1){
let episodeCount = 0;
document.querySelectorAll("ul.stats > li").forEach((item) => {
let strongItem = item.querySelector("strong");
if(strongItem !== null && strongItem.textContent === "Episodes:"){
episodeCount = parseInt(item.querySelector("a").textContent);
}
});
if(episodeCount === 0 && (torrentType === "Movie")){
episodeCount = 1;
}
let torrentListing = [];
document.querySelectorAll(".group_torrent").forEach((item) => {
const linkSection = item.querySelector("td");
const downloadLink = item.querySelector("a[title^='Download']");
const [torrentEntry] = Array.from(linkSection.querySelectorAll("a")).slice(-1);
const torrentId = parseInt(downloadLink.getAttribute("href").match(/\/torrent\/(?<torrent_id>[0-9]+)\/download/).groups.torrent_id);
let tags = {
audioCount: 1,
freeleech: torrentEntry.querySelector("img") !== null,
snatched: torrentEntry.textContent.match(/ - Snatched/) !== null
};
torrentEntry.textContent.replace("»", "").replace(" - Snatched", "").split(" | ").forEach((t) => {
let tagEntry = t.trim();
if(tagEntry === ""){
return;
}
let match = null;
if(!("source" in tags) && knownSources.indexOf(tagEntry) !== -1){
tags.source = tagEntry;
}else if(!("container" in tags) && knownContainers.some((i) => tagEntry.startsWith(i))){
if((match = tagEntry.match(/^(?<container>[^(]+) \((?<region>[^)]+)\)$/)) !== null){
tags.container = match.groups.container;
tags.region = match.groups.region;
}else{
tags.container = tagEntry;
}
}else if(!("videoCodec" in tags) && knownVideoCodecs.some((i) => tagEntry.startsWith(i))){
tags.videoCodec = tagEntry;
}else if(!("audioCodec" in tags) && knownAudioCodecs.some((i) => tagEntry.startsWith(i))){
match = tagEntry.match(/^(?<codec>.+) (?<channels>[0-9.]+)$/)
tags.audioCodec = match.groups.codec;
tags.audioChannels = match.groups.channels.split(".").reduce((p, c) => parseInt(p) + parseInt(c));
}else if(tagEntry.match(/^[0-9]+:[0-9]+$/) !== null){
tags.aspectRatio = tagEntry;
}else if(tagEntry.match(/^[0-9]+x[0-9]+$/) !== null || tagEntry.match(/^(720p|1080p|1080i|4K)$/) !== null){
tags.resolution = tagEntry;
}else if(tagEntry === "Dual Audio"){
tags.audioCount = 2;
}else if((match = tagEntry.match(/^(?<kind>(RAW|Hardsubs|Softsubs))( \()?(?<group>[^)]*)(\))?$/)) !== null){
if(match.groups.kind !== "RAW"){
tags.subtitleType = match.groups.kind;
}
if(match.groups.group !== ""){
tags.group = match.groups.group;
}
}else{
//console.log(tagEntry);
}
});
let torrent = {
id: torrentId,
tags: tags,
elements: {
item: item,
entry: torrentEntry,
link: downloadLink,
},
};
const dataSection = document.getElementById("torrent_" + torrentId);
if(dataSection !== null){
const descriptionSection = document.getElementById(torrentId + "_description");
const filelistSection = document.getElementById(torrentId + "_filelist");
const mediaInfoSection = document.getElementById(torrentId + "_mediainfo");
if(descriptionSection !== null){
torrent.elements.description = descriptionSection.querySelector("blockquote");
torrent.description = torrent.elements.description.textContent;
}
if(filelistSection !== null){
torrent.elements.filelist = filelistSection.querySelector("table");
torrent.filelist = [];
torrent.elements.filelist.querySelectorAll("tr").forEach((tableRow) => {
if(tableRow.querySelector("td > strong") !== null){
//Skip header
return;
}
torrent.filelist.push({
path: tableRow.querySelector("td").textContent,
size: tableRow.querySelector("td:last-of-type").textContent //TODO: convert back to bytes
})
});
}
if(mediaInfoSection !== null){
torrent.elements.mediainfo = mediaInfoSection;
let m_list = [];
torrent.elements.mediainfo.querySelectorAll("pre").forEach((mediainfoItem) => {
let m = parseMediaInfo(mediainfoItem.textContent);
if(m !== null){
m_list.push(m);
}
});
if(m_list.length > 0){
torrent.mediainfo = m_list;
}
}
torrentListing.push(torrent);
}
});
console.log(torrentListing);
torrentListing.forEach((torrent) => {
try {
if("mediainfo" in torrent && torrent.mediainfo.length > 0){
let mediainfo = torrent.mediainfo[0];
let japaneseAudio = null;
let otherAudio = null;
let defaultAudio = null;
let englishSubs = null;
let defaultSubs = null;
let tags = {
};
let warnings = {
general: [],
audio: [],
video: [],
text: []
}
switch(mediainfo.general.format){
case "Matroska":
tags.container = "MKV";
break;
case "BDAV":
tags.container = "M2TS";
break;
}
let fileName = null;
if("mediainfo.general.complete_name" in mediainfo.general){
[fileName] = mediainfo.general.complete_name.split("/").slice(-1);
}else{
fileName = torrent.filelist[0].path;
}
//handle file tags
fileName.split(".").forEach((p) => {
if(p === "NF"){
tags.sourceName = "Netflix";
}else if(p === "ATVP"){
tags.sourceName = "Apple TV+";
}else if(p === "AMZN"){
tags.sourceName = "Amazon Video";
}else if(p === "HDR"){
tags.videoHDR = "HDR";
}else if(p === "HDR10"){
tags.videoHDR = "HDR10";
}else if(p === "DV"){
tags.videoHDR = "HDR DV";
}
});
fileName.split(" ").forEach((p) => {
if(p === "HDR"){
tags.videoHDR = "HDR";
}else if(p === "HDR10"){
tags.videoHDR = "HDR10";
}else if(p === "DV"){
tags.videoHDR = "HDR DV";
}
});
mediainfo.audio.forEach((audio) => {
const isDefault = audio.default === "Yes";
if(isDefault){
defaultAudio = audio;
}
if("language" in audio && audio.language !== "Japanese" && audio.language !== "Unknown" && isDefault && torrentType.match(/Live Action/) === null){
warnings.audio.push("Default audio is not in Japanese");
}
if(audio.language === "Japanese"){
if(japaneseAudio === null){ // Pick first
japaneseAudio = audio;
}else{
const oldChannels = parseInt(japaneseAudio.channels.replace(" channels", ""));
const channels = parseInt(audio.channels.replace(" channels", ""));
if(channels > oldChannels){
japaneseAudio = audio;
}
}
}else if(mediainfo.audio.length === 1 && (audio.language === "Unknown" || !("language" in audio))){
japaneseAudio = audio;
}else{
if(otherAudio === null){ // Pick first
otherAudio = audio;
}else{
const oldChannels = parseInt(otherAudio.channels.replace(" channels", ""));
const channels = parseInt(audio.channels.replace(" channels", ""));
if(channels > oldChannels){
otherAudio = audio;
}
}
}
});
if(japaneseAudio !== otherAudio && otherAudio !== null && japaneseAudio !== null){
tags.audioCount = mediainfo.audio.length;
}
if("text" in mediainfo){
mediainfo.text.forEach((text) => {
const isSignsEntry = ("title" in text) ? text.title.match(/sign/i) !== null : false;
const isDefault = text.default === "Yes";
if(isSignsEntry && isDefault){
warnings.text.push("Default subtitle is a Signs track");
}
if(isDefault){
defaultSubs = text;
}
if(!isSignsEntry && text.language === "English"){
if(englishSubs === null){
englishSubs = text;
}else{
if(text.codec_id === "S_TEXT/ASS" && englishSubs.codec_id !== "S_TEXT/ASS"){
englishSubs = text;
}
}
}else if(mediainfo.text.length === 1 && (text.language === "Unknown" || !("language" in text))){
englishSubs = text;
}
});
}
let audio = japaneseAudio !== null ? japaneseAudio : (defaultAudio !== null ? defaultAudio : otherAudio);
if(audio !== null){
tags.audioChannels = parseInt(audio.channels.replace(" channels", ""));
audioCodecs.every((codec) => {
let match = true;
for(const [key, value] of Object.entries(codec.match)){
if(value === null){
if(key in audio){
match = false;
console.log("audio[" + key + "] exists but value was null");
break;
}
continue;
}
if(!(key in audio)){
match = false;
console.log("audio[" + key + "] does not exist but value was not null");
break;
}else if (value instanceof RegExp && audio[key].match(value) === null){
match = false;
console.log("audio[" + key + "] ("+ audio[key] +") ~= " + value.toString());
break;
}else if (!(value instanceof RegExp) && audio[key] !== value){
match = false;
console.log("audio[" + key + "] ("+ audio[key] +") !== " + value);
break;
}
}
if(match){
tags.audioCodec = codec;
}
return !match;
});
}
let video = mediainfo.video[0];
if(video !== null){
videoCodecs.every((codec) => {
let match = true;
for(const [key, value] of Object.entries(codec.match)){
if(value === null){
if(key in video){
match = false;
break;
}
continue;
}
if(!(key in video)){
match = false;
break;
}else if (value instanceof RegExp && video[key].match(value) === null){
match = false;
break;
}else if (!(value instanceof RegExp) && video[key] !== value){
match = false;
break;
}
}
if(match){
tags.videoCodec = codec;
}
return !match;
});
let width = parseInt(video.width.replace(/[^0-9]/g, ""));
let height = parseInt(video.height.replace(/[^0-9]/g, ""));
if(width === 1280 && height > 480 && height <= 720){
tags.resolution = "720" + ((!("scan_type" in video) || video.scan_type === "Progressive") ? "p" : "i");
}else if(width === 1920 && height > 720 && height <= 1080){
tags.resolution = "1080" + ((!("scan_type" in video) || video.scan_type === "Progressive") ? "p" : "i");
}else if(width === 3840 && height > 1080 && height <= 2160){
tags.resolution = "4K";
}else{
tags.resolution = width + "x" + height;
}
if("color_primaries" in video && video.color_primaries === "BT.2020"){
tags.videoHDR = "HDR";
}
if("hdr_format" in video){
if(video.hdr_format.match(/Dolby Vision/) !== null){
tags.videoHDR = "HDR DV";
}else if(video.hdr_format.match(/SMPTE ST 2086/) !== null && video.hdr_format.match(/HDR10/) !== null){
tags.videoHDR = "HDR10";
}else if(video.hdr_format.match(/SMPTE ST 2094-40/) !== null && video.hdr_format.match(/HDR10/) !== null){
tags.videoHDR = "HDR10+";
}else if(video.hdr_format.match(/SL-HDR1/) !== null){
tags.videoHDR = "SL-HDR1";
}else if(video.hdr_format.match(/SL-HDR2/) !== null){
tags.videoHDR = "SL-HDR2";
}
}
if("writing_library" in video){
if(video.writing_library.match(/x265/) !== null){
tags.videoEncoder = "x265";
}else if(video.writing_library.match(/x264/) !== null){
tags.videoEncoder = "x264";
}else if(video.writing_library.match(/_nvenc/) !== null){
tags.videoEncoder = "NVENC";
warnings.video.push("Found NVENC hardware-encoded stream");
}else if(video.writing_library.match(/_qsv/) !== null){
tags.videoEncoder = "QuickSync";
warnings.video.push("Found QuickSync hardware-encoded stream");
}else if(video.writing_library.match(/_vaapi/) !== null){
tags.videoEncoder = "VAAPI";
warnings.video.push("Found VAAPI hardware-encoded stream");
}else if(video.writing_library.match(/[^0-9A-F]/) !== null){
[tags.videoEncoder] = video.writing_library.split(" ");
}
}
if("encoding_settings" in video){
let match = null;
if((match = video.encoding_settings.match(/crf=(?<crf>[0-9.]+)/)) !== null){
tags.videoCRF = parseFloat(match.groups.crf).toFixed(1);
}
if(video.encoding_settings.match(/rc=2pass/) !== null){
tags.videoCRF = "2pass";
}
if((match = video.encoding_settings.match(/profile=(?<profile>baseline|main)/)) !== null){
warnings.video.push("Found encode using profile=" + match.groups.profile + " on encoding settings");
}
if(video.encoding_settings.match(/cabac=0/) !== null){
warnings.video.push("Found encode with CABAC disabled");
}
if(video.encoding_settings.match(/mbtree=0/) !== null){
warnings.video.push("Found encode with mbtree disabled");
}
if(video.encoding_settings.match(/bframes=0/) !== null){
warnings.video.push("Found encode with bframes disabled");
}
if(video.encoding_settings.match(/me=dia/) !== null){
warnings.video.push("Found encode with me=dia");
}
}
}
let text = englishSubs !== null ? englishSubs : (defaultSubs !== null ? defaultSubs : (("text" in mediainfo && mediainfo.text.length > 0) ? mediainfo.text[0] : null));
if(text !== null){
tags.subtitleCodec = text.format;
tags.subtitleTitle = "title" in text;
tags.subtitleType = "Softsubs";
}
for(const [key, value] of Object.entries(torrent.tags)){
if(!(key in tags)){
tags[key] = value;
}
}
let oldTagLine = getComparisonLine(torrent.tags);
let newTagLine = getComparisonLine(tags);
if(newTagLine !== oldTagLine){
warnings.general.push("Tag mismatch:\nold: " + oldTagLine + " !=\nnew: " + newTagLine);
}
if(tags.freeleech){
let fl = torrent.elements.entry.querySelector("img");
torrent.elements.entry.textContent = getEntryLine(tags) + " | ";
torrent.elements.entry.append(fl);
}else{
torrent.elements.entry.textContent = getEntryLine(tags);
}
if(tags.snatched){
torrent.elements.entry.append(" - Snatched");
}
let warningText = "";
for(const [key, value] of Object.entries(warnings)){
if(value.length > 0){
warningText += key.toUpperCase() + ":\n";
for(let v of value){
warningText += " " + v + "\n";
}
warningText += "\n";
}
}
if(warningText !== ""){
torrent.elements.item.style["font-style"] = "italic";
let pre = document.createElement("pre");
pre.textContent = "== WARNINGS ==\n\n" + warningText + "\n";
if("description" in torrent.elements){
torrent.elements.description.prepend(pre);
}
torrent.elements.entry.prepend(" - ");
for(let i = 0; i < 3; ++i){
let warn = document.createElement("img");
warn.src = "/static/common/symbols/warned.png";
torrent.elements.entry.prepend(warn);
}
}
}else{
torrent.elements.item.style["font-style"] = "italic";
torrent.elements.item.style["text-decoration"] = "line-through";
let pre = document.createElement("pre");
pre.textContent = "== WARNINGS ==\n\nNo valid mediainfo section found. Maybe check Description?\n\n";
if("description" in torrent.elements){
torrent.elements.description.prepend(pre);
}
torrent.elements.entry.prepend(" - ");
for(let i = 0; i < 3; ++i){
let warn = document.createElement("img");
warn.src = "/static/common/symbols/warned.png";
torrent.elements.entry.prepend(warn);
}
}
}catch (e){
console.log("Error:", e);
console.log(torrent);
}
});
}
//21:28:16 <+AMM> subs, signage, chapters, titles, codecs, containers
// 21:28:20 <+AMM> What is good, what is bad

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2021 WeebDataHoarder
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: