mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Remove Beth.NET and YT downloader, no one used them and they only sortof worked
This commit is contained in:
parent
4da8fbb9f7
commit
f129a6bd22
@ -39,8 +39,6 @@ namespace Wabbajack.Lib.Downloaders
|
||||
typeof(DeadlyStreamDownloader.State),
|
||||
typeof(TESAllianceDownloader.State),
|
||||
typeof(TESAllDownloader.State),
|
||||
typeof(BethesdaNetDownloader.State),
|
||||
typeof(YouTubeDownloader.State),
|
||||
typeof(YandexDownloader.State),
|
||||
typeof(WabbajackCDNDownloader.State)
|
||||
};
|
||||
|
Binary file not shown.
@ -1,123 +0,0 @@
|
||||
"""
|
||||
Simple process that uses Fria to inspect the HTTP headers, and sent data during a Skyrim/Fallout login to Bethesda.NET. That data is output to the screen and can later be used to
|
||||
allow Wabbajack to log into Bethesda.NET.
|
||||
"""
|
||||
|
||||
import frida
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
import psutil, time, json
|
||||
from pathlib import Path
|
||||
|
||||
known_headers = {}
|
||||
shutdown = False
|
||||
|
||||
|
||||
def on_message(message, data):
|
||||
msg_type, msg_data = message["payload"]
|
||||
if msg_type == "header":
|
||||
header, value = msg_data.split(": ")
|
||||
if header not in known_headers:
|
||||
known_headers[header] = value
|
||||
if msg_type == "data":
|
||||
try:
|
||||
data = json.loads(msg_data)
|
||||
if "scheme" in data and "language" in data and "payload" in data:
|
||||
shutdown_and_print(data)
|
||||
except:
|
||||
return
|
||||
|
||||
|
||||
def main(target_process):
|
||||
session = frida.attach(target_process)
|
||||
|
||||
script = session.create_script("""
|
||||
|
||||
// Find base address of current imported jvm.dll by main process fledge.exe
|
||||
var reqHeaders = Module.getExportByName('winhttp.dll', 'WinHttpAddRequestHeaders');
|
||||
|
||||
Interceptor.attach(reqHeaders, { // Intercept calls to our SetAesDecrypt function
|
||||
|
||||
// When function is called, print out its parameters
|
||||
onEnter: function (args) {
|
||||
send(['header', args[1].readUtf16String(args[2].toInt32())]);
|
||||
|
||||
},
|
||||
// When function is finished
|
||||
onLeave: function (retval) {
|
||||
}
|
||||
});
|
||||
|
||||
var reqHeaders = Module.getExportByName('winhttp.dll', 'WinHttpWriteData');
|
||||
console.log("WinHttpAddRequestHeaders: " + reqHeaders);
|
||||
|
||||
Interceptor.attach(reqHeaders, { // Intercept calls to our SetAesDecrypt function
|
||||
|
||||
// When function is called, print out its parameters
|
||||
onEnter: function (args) {
|
||||
send(['data', args[1].readUtf8String(args[2].toInt32())]);
|
||||
|
||||
},
|
||||
// When function is finished
|
||||
onLeave: function (retval) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
""")
|
||||
script.on('message', on_message)
|
||||
script.load()
|
||||
|
||||
while not shutdown and psutil.pid_exists(target_process):
|
||||
time.sleep(0.5)
|
||||
|
||||
session.detach()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def wait_for_game(started, name):
|
||||
no_exe = 0
|
||||
parent_path = Path(started).parent
|
||||
while True:
|
||||
found = False
|
||||
time.sleep(1)
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
if Path(proc.exe()).parent == parent_path:
|
||||
no_exe = 0
|
||||
found = True
|
||||
except:
|
||||
pass
|
||||
if proc.name() == name:
|
||||
return proc.pid
|
||||
|
||||
if not found:
|
||||
print("Not Found " + str(no_exe))
|
||||
no_exe += 1
|
||||
if no_exe == 3:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def shutdown_and_print(data):
|
||||
global shutdown
|
||||
output = {"body": json.dumps(data), "headers": known_headers}
|
||||
|
||||
print(json.dumps(output))
|
||||
|
||||
for proc in psutil.process_iter():
|
||||
if proc.pid == pid:
|
||||
proc.kill()
|
||||
break
|
||||
|
||||
shutdown = True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start = """C:\Steam\steamapps\common\Skyrim Special Edition\SkyrimSE.exe"""
|
||||
wait_for = "SkyrimSE.exe"
|
||||
if len(sys.argv) == 3:
|
||||
start = sys.argv[1]
|
||||
wait_for = sys.argv[2]
|
||||
target_process = Popen([start])
|
||||
pid = wait_for_game(start, wait_for)
|
||||
main(pid)
|
@ -1,386 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.Serialization.Json;
|
||||
using Wabbajack.Lib.Validation;
|
||||
using File = Alphaleonis.Win32.Filesystem.File;
|
||||
using Game = Wabbajack.Common.Game;
|
||||
using Path = Alphaleonis.Win32.Filesystem.Path;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders
|
||||
{
|
||||
public class BethesdaNetDownloader : IUrlDownloader, INeedsLogin
|
||||
{
|
||||
private bool _isPrepared;
|
||||
public const string DataName = "bethesda-net-data";
|
||||
|
||||
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
|
||||
public IObservable<bool> IsLoggedIn => Utils.HaveEncryptedJsonObservable(DataName);
|
||||
public string SiteName => "Bethesda.NET";
|
||||
public IObservable<string> MetaInfo => Observable.Return(""); //"Wabbajack will start the game, then exit once you enter the Mods page";
|
||||
public Uri SiteURL => new Uri("https://bethesda.net");
|
||||
public Uri? IconUri => new Uri("https://bethesda.net/favicon.ico");
|
||||
|
||||
public BethesdaNetDownloader()
|
||||
{
|
||||
TriggerLogin = ReactiveCommand.CreateFromTask(() => Utils.CatchAndLog(RequestLoginAndCache), IsLoggedIn.Select(b => !b).ObserveOn(RxApp.MainThreadScheduler));
|
||||
ClearLogin = ReactiveCommand.CreateFromTask(() => Utils.CatchAndLog(async () => await Utils.DeleteEncryptedJson(DataName)), IsLoggedIn.ObserveOn(RxApp.MainThreadScheduler));
|
||||
}
|
||||
|
||||
private static async Task RequestLoginAndCache()
|
||||
{
|
||||
await Utils.Log(new RequestBethesdaNetLogin()).Task;
|
||||
}
|
||||
|
||||
public async Task<AbstractDownloadState?> GetDownloaderState(dynamic archiveINI, bool quickMode)
|
||||
{
|
||||
var url = (Uri)DownloaderUtils.GetDirectURL(archiveINI);
|
||||
return StateFromUrl(url);
|
||||
}
|
||||
|
||||
internal static AbstractDownloadState? StateFromUrl(Uri url)
|
||||
{
|
||||
if (url != null && url.Host == "bethesda.net" && url.AbsolutePath.StartsWith("/en/mods/"))
|
||||
{
|
||||
var split = url.AbsolutePath.Split('/');
|
||||
var game = split[3];
|
||||
var modId = split[5];
|
||||
return new State(gameName: game, contentId: modId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task Prepare()
|
||||
{
|
||||
if (_isPrepared) return;
|
||||
if (Utils.HaveEncryptedJson(DataName))
|
||||
{
|
||||
_isPrepared = true;
|
||||
return;
|
||||
}
|
||||
await Utils.Log(new RequestBethesdaNetLogin()).Task;
|
||||
_isPrepared = true;
|
||||
}
|
||||
|
||||
public static async Task<BethesdaNetData?> Login(Game game)
|
||||
{
|
||||
var metadata = game.MetaData();
|
||||
if (metadata.MainExecutable == null) throw new NotImplementedException();
|
||||
var gamePath = metadata.GameLocation().Combine(metadata.MainExecutable);
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = @"Downloaders\BethesdaNet\bethnetlogin.exe",
|
||||
Arguments = $"\"{gamePath}\" {metadata.MainExecutable}",
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var process = Process.Start(info);
|
||||
ChildProcessTracker.AddProcess(process);
|
||||
string last_line = "";
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = await process.StandardOutput.ReadLineAsync();
|
||||
if (line == null) break;
|
||||
last_line = line;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = last_line.FromJsonString<BethesdaNetData>();
|
||||
await result.ToEcryptedJson(DataName);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.Error(ex, "Could not save Bethesda.NET login info");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public AbstractDownloadState? GetDownloaderState(string url)
|
||||
{
|
||||
return StateFromUrl(new Uri(url));
|
||||
}
|
||||
|
||||
[JsonName("BethesdaNetDownloader")]
|
||||
public class State : AbstractDownloadState
|
||||
{
|
||||
public string GameName { get; }
|
||||
public string ContentId { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override object[] PrimaryKey => new object[] { GameName, ContentId };
|
||||
|
||||
public State(string gameName, string contentId)
|
||||
{
|
||||
GameName = gameName;
|
||||
ContentId = contentId;
|
||||
}
|
||||
|
||||
public override bool IsWhitelisted(ServerWhitelist whitelist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
||||
{
|
||||
var (client, info, collected) = await ResolveDownloadInfo();
|
||||
await using var tf = new TempFile();
|
||||
await using var file = tf.File.Create();
|
||||
var max_chunks = info.depot_list[0].file_list[0].chunk_count;
|
||||
foreach (var chunk in info.depot_list[0].file_list[0].chunk_list.OrderBy(c => c.index))
|
||||
{
|
||||
Utils.Status($"Downloading {a.Name}", Percent.FactoryPutInRange(chunk.index, max_chunks));
|
||||
using var got = await client.GetAsync(
|
||||
$"https://content.cdp.bethesda.net/{collected.CDPProductId}/{collected.CDPPropertiesId}/{chunk.sha}");
|
||||
var data = await got.Content.ReadAsByteArrayAsync();
|
||||
if (collected.AESKey != null)
|
||||
AESCTRDecrypt(collected.AESKey, collected.AESIV!, data);
|
||||
|
||||
if (chunk.uncompressed_size == chunk.chunk_size)
|
||||
await file.WriteAsync(data, 0, data.Length);
|
||||
else
|
||||
{
|
||||
using (var ms = new MemoryStream(data))
|
||||
using (var zlibStream =
|
||||
new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream(ms))
|
||||
await zlibStream.CopyToAsync(file);
|
||||
}
|
||||
}
|
||||
file.Close();
|
||||
await ConvertCKMToZip((AbsolutePath)file.Name, destination);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private const uint CKM_Magic = 0x52415442; // BTAR
|
||||
private async Task ConvertCKMToZip(AbsolutePath src, AbsolutePath dest)
|
||||
{
|
||||
using var reader = new BinaryReader(await src.OpenRead());
|
||||
var magic = reader.ReadUInt32();
|
||||
if (magic != CKM_Magic)
|
||||
throw new InvalidDataException("Invalid magic format in CKM parsing");
|
||||
|
||||
ushort majorVersion = reader.ReadUInt16();
|
||||
ushort minorVersion = reader.ReadUInt16();
|
||||
if (majorVersion != 1)
|
||||
throw new InvalidDataException("Archive major version is unknown. Should be 1.");
|
||||
|
||||
if (minorVersion < 2 || minorVersion > 4)
|
||||
throw new InvalidDataException("Archive minor version is unknown. Should be 2, 3, or 4.");
|
||||
|
||||
await using var fos = await dest.Create();
|
||||
using var archive = new ZipArchive(fos, ZipArchiveMode.Create);
|
||||
while (reader.PeekChar() != -1)
|
||||
{
|
||||
ushort nameLength = reader.ReadUInt16();
|
||||
string name = Encoding.UTF8.GetString(reader.ReadBytes(nameLength));
|
||||
ulong dataLength = reader.ReadUInt64();
|
||||
|
||||
if (dataLength > int.MaxValue)
|
||||
throw new Exception();
|
||||
|
||||
var entry = archive.CreateEntry(name, CompressionLevel.NoCompression);
|
||||
await using var output = entry.Open();
|
||||
await reader.BaseStream.CopyToLimitAsync(output, (long)dataLength);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<bool> Verify(Archive archive)
|
||||
{
|
||||
await ResolveDownloadInfo();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<(Wabbajack.Lib.Http.Client, CDPTree, CollectedBNetInfo)> ResolveDownloadInfo()
|
||||
{
|
||||
var info = new CollectedBNetInfo();
|
||||
|
||||
var login_info = await Utils.FromEncryptedJson<BethesdaNetData>(DataName);
|
||||
|
||||
var client = new Wabbajack.Lib.Http.Client();
|
||||
|
||||
client.Headers.Add(("User-Agent", "bnet"));
|
||||
foreach (var header in login_info.headers.Where(h => h.Key.ToLower().StartsWith("x-")))
|
||||
client.Headers.Add((header.Key, header.Value));
|
||||
|
||||
var posted = await client.PostAsync("https://api.bethesda.net/beam/accounts/external_login",
|
||||
new StringContent(login_info.body, Encoding.UTF8, "application/json"));
|
||||
|
||||
info.AccessToken = (await posted.Content.ReadAsStringAsync()).FromJsonString<BeamLoginResponse>().access_token;
|
||||
|
||||
client.Headers.Add(("x-cdp-app", "UGC SDK"));
|
||||
client.Headers.Add(("x-cdp-app-ver", "0.9.11314/debug"));
|
||||
client.Headers.Add(("x-cdp-lib-ver", "0.9.11314/debug"));
|
||||
client.Headers.Add(("x-cdp-platform", "Win/32"));
|
||||
|
||||
posted = await client.PostAsync("https://api.bethesda.net/cdp-user/auth",
|
||||
new StringContent("{\"access_token\": \"" + info.AccessToken + "\"}", Encoding.UTF8,
|
||||
"application/json"));
|
||||
info.CDPToken = (await posted.Content.ReadAsStringAsync()).FromJsonString<CDPLoginResponse>().token;
|
||||
|
||||
client.Headers.Add(("X-Access-Token", info.AccessToken));
|
||||
var got = await client.GetAsync($"https://api.bethesda.net/mods/ugc-workshop/content/get?content_id={ContentId}");
|
||||
JObject data = JObject.Parse(await got.Content.ReadAsStringAsync());
|
||||
|
||||
var content = data["platform"]!["response"]!["content"]!;
|
||||
|
||||
info.CDPBranchId = (int)content["cdp_branch_id"]!;
|
||||
info.CDPProductId = (int)content["cdp_product_id"]!;
|
||||
|
||||
client.Headers.Add(("Authorization", $"Token {info.CDPToken}"));
|
||||
client.Headers.Add(("Accept", "application/json"));
|
||||
|
||||
got.Dispose();
|
||||
got = await client.GetAsync(
|
||||
$"https://api.bethesda.net/cdp-user/projects/{info.CDPProductId}/branches/{info.CDPBranchId}/tree/.json");
|
||||
|
||||
var tree = (await got.Content.ReadAsStringAsync()).FromJsonString<CDPTree>();
|
||||
|
||||
got.Dispose();
|
||||
got = await client.PostAsync($"https://api.bethesda.net/mods/ugc-content/add-subscription", new StringContent($"{{\"content_id\": \"{ContentId}\"}}", Encoding.UTF8, "application/json"));
|
||||
|
||||
got.Dispose();
|
||||
got = await client.GetAsync(
|
||||
$"https://api.bethesda.net/cdp-user/projects/{info.CDPProductId}/branches/{info.CDPBranchId}/depots/.json");
|
||||
|
||||
var props_obj = JObject.Parse(await got.Content.ReadAsStringAsync()).Properties().First();
|
||||
info.CDPPropertiesId = (int)props_obj.Value["properties_id"]!;
|
||||
|
||||
info.AESKey = props_obj.Value["ex_info_A"].Select(e => (byte)e).ToArray();
|
||||
info.AESIV = props_obj.Value["ex_info_B"].Select(e => (byte)e).Take(16).ToArray();
|
||||
|
||||
return (client, tree, info);
|
||||
}
|
||||
|
||||
static int AESCTRDecrypt(byte[] Key, byte[] IV, byte[] Data)
|
||||
{
|
||||
IBufferedCipher cipher = CipherUtilities.GetCipher("AES/CTR/NoPadding");
|
||||
cipher.Init(false, new ParametersWithIV(ParameterUtilities.CreateKeyParameter("AES", Key), IV));
|
||||
|
||||
return cipher.DoFinal(Data, Data, 0);
|
||||
}
|
||||
|
||||
public override IDownloader GetDownloader()
|
||||
{
|
||||
return DownloadDispatcher.GetInstance<BethesdaNetDownloader>();
|
||||
}
|
||||
|
||||
public override string GetManifestURL(Archive a)
|
||||
{
|
||||
return $"https://bethesda.net/en/mods/{GameName}/mod-detail/{ContentId}";
|
||||
}
|
||||
|
||||
public override string[] GetMetaIni()
|
||||
{
|
||||
return new[] { "[General]", $"directURL=https://bethesda.net/en/mods/{GameName}/mod-detail/{ContentId}" };
|
||||
}
|
||||
|
||||
|
||||
private class BeamLoginResponse
|
||||
{
|
||||
public string access_token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class CDPLoginResponse
|
||||
{
|
||||
public string token { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private class CollectedBNetInfo
|
||||
{
|
||||
public byte[] AESKey { get; set; } = null!;
|
||||
public byte[] AESIV { get; set; } = null!;
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public string CDPToken { get; set; } = string.Empty;
|
||||
public int CDPBranchId { get; set; }
|
||||
public int CDPProductId { get; set; }
|
||||
public int CDPPropertiesId { get; set; }
|
||||
}
|
||||
|
||||
public class CDPTree
|
||||
{
|
||||
public List<Depot> depot_list { get; set; } = null!;
|
||||
|
||||
public class Depot
|
||||
{
|
||||
public List<CDPFile> file_list { get; set; } = null!;
|
||||
|
||||
public class CDPFile
|
||||
{
|
||||
public int chunk_count { get; set; }
|
||||
public List<Chunk> chunk_list { get; set; } = null!;
|
||||
|
||||
public string? name { get; set; }
|
||||
|
||||
public class Chunk
|
||||
{
|
||||
public int chunk_size { get; set; }
|
||||
public int index { get; set; }
|
||||
public string sha { get; set; } = string.Empty;
|
||||
public int uncompressed_size { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal class DownloadInfo
|
||||
{
|
||||
}
|
||||
|
||||
public class RequestBethesdaNetLogin : AUserIntervention
|
||||
{
|
||||
public override string ShortDescription => "Logging into Bethesda.NET";
|
||||
public override string ExtendedDescription { get; } = string.Empty;
|
||||
|
||||
private readonly TaskCompletionSource<BethesdaNetData> _source = new TaskCompletionSource<BethesdaNetData>();
|
||||
public Task<BethesdaNetData> Task => _source.Task;
|
||||
|
||||
public void Resume(BethesdaNetData data)
|
||||
{
|
||||
Handled = true;
|
||||
_source.SetResult(data);
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
Handled = true;
|
||||
_source.SetCanceled();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[JsonName("BethesdaNetData")]
|
||||
public class BethesdaNetData
|
||||
{
|
||||
public string body { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> headers = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
@ -24,10 +24,8 @@ namespace Wabbajack.Lib.Downloaders
|
||||
new LoversLabDownloader(),
|
||||
new VectorPlexusDownloader(),
|
||||
new DeadlyStreamDownloader(),
|
||||
new BethesdaNetDownloader(),
|
||||
new TESAllianceDownloader(),
|
||||
new TESAllDownloader(),
|
||||
new YouTubeDownloader(),
|
||||
new WabbajackCDNDownloader(),
|
||||
new YandexDownloader(),
|
||||
new HTTPDownloader(),
|
||||
@ -36,8 +34,6 @@ namespace Wabbajack.Lib.Downloaders
|
||||
|
||||
public static readonly List<IUrlInferencer> Inferencers = new List<IUrlInferencer>()
|
||||
{
|
||||
new BethesdaNetInferencer(),
|
||||
new YoutubeInferencer(),
|
||||
new WabbajackCDNInfluencer()
|
||||
};
|
||||
|
||||
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders.UrlDownloaders
|
||||
{
|
||||
public class BethesdaNetInferencer : IUrlInferencer
|
||||
{
|
||||
public async Task<AbstractDownloadState?> Infer(Uri uri)
|
||||
{
|
||||
return BethesdaNetDownloader.StateFromUrl(uri);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DynamicData;
|
||||
using Org.BouncyCastle.Utilities.Collections;
|
||||
using Wabbajack.Common;
|
||||
using YoutubeExplode;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders.UrlDownloaders
|
||||
{
|
||||
public class YoutubeInferencer : IUrlInferencer
|
||||
{
|
||||
public async Task<AbstractDownloadState?> Infer(Uri uri)
|
||||
{
|
||||
var state = YouTubeDownloader.UriToState(uri) as YouTubeDownloader.State;
|
||||
if (state == null) return null;
|
||||
|
||||
var client = new YoutubeClient(Wabbajack.Lib.Http.ClientFactory.Client);
|
||||
var video = await client.Videos.GetAsync(state.Key);
|
||||
|
||||
var desc = video.Description;
|
||||
|
||||
var replaceChars = new HashSet<char>() {'_', '(', ')', '-'};
|
||||
|
||||
var lines = desc.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.Trim())
|
||||
.Select(line =>
|
||||
{
|
||||
|
||||
var segments = replaceChars.Aggregate(line, (acc, c) => acc.Replace(c, ' '))
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0) return (TimeSpan.Zero, string.Empty);
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (TryParseEx(segment, out var si))
|
||||
{
|
||||
return (si, string.Join(" ", segments.Where(s => !s.Contains(":"))));
|
||||
}
|
||||
}
|
||||
return (TimeSpan.Zero, string.Empty);
|
||||
})
|
||||
.Where(t => t.Item2 != string.Empty)
|
||||
.ToList();
|
||||
|
||||
var tracks = lines.Select((line, idx) => new YouTubeDownloader.State.Track
|
||||
{
|
||||
Name = Sanitize(line.Item2),
|
||||
Start = line.Item1,
|
||||
End = idx < lines.Count - 1 ? lines[idx + 1].Item1 : video.Duration,
|
||||
Format = YouTubeDownloader.State.Track.FormatEnum.XWM
|
||||
}).ToList();
|
||||
|
||||
foreach (var track in tracks)
|
||||
{
|
||||
Utils.Log($"Inferred Track {track.Name} {track.Format} {track.Start}-{track.End}");
|
||||
}
|
||||
|
||||
state.Tracks = tracks;
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private string Sanitize(string input)
|
||||
{
|
||||
return input.Replace(":", "_").Replace("'", "").Replace("\"", "");
|
||||
}
|
||||
|
||||
private static bool TryParseEx(string s, out TimeSpan span)
|
||||
{
|
||||
var ints = s.Split(':').Select(segment => int.TryParse(segment, out int v) ? v : -1).ToArray();
|
||||
if (ints.Any(i => i == -1))
|
||||
{
|
||||
span = TimeSpan.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (ints.Length)
|
||||
{
|
||||
case 2:
|
||||
span = new TimeSpan(0, ints[0], ints[1]);
|
||||
break;
|
||||
case 3:
|
||||
span = new TimeSpan(ints[0], ints[1], ints[2]);
|
||||
break;
|
||||
default:
|
||||
span = TimeSpan.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,250 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.Serialization.Json;
|
||||
using Wabbajack.Lib.Validation;
|
||||
using YoutubeExplode;
|
||||
using YoutubeExplode.Exceptions;
|
||||
using YoutubeExplode.Videos.Streams;
|
||||
using File = Alphaleonis.Win32.Filesystem.File;
|
||||
using Path = Alphaleonis.Win32.Filesystem.Path;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders
|
||||
{
|
||||
public class YouTubeDownloader : IDownloader
|
||||
{
|
||||
public async Task<AbstractDownloadState?> GetDownloaderState(dynamic archiveINI, bool quickMode)
|
||||
{
|
||||
var directURL = (Uri)DownloaderUtils.GetDirectURL(archiveINI);
|
||||
var state = UriToState(directURL) as State;
|
||||
if (state == null) return state;
|
||||
|
||||
var idx = 0;
|
||||
while (true)
|
||||
{
|
||||
var section = archiveINI[$"track/{idx}"];
|
||||
if (section.name == null) break;
|
||||
|
||||
var track = new State.Track();
|
||||
track.Name = section.name;
|
||||
track.Start = TimeSpan.Parse(section.start);
|
||||
track.End = TimeSpan.Parse(section.end);
|
||||
track.Format = Enum.Parse<State.Track.FormatEnum>(section.format);
|
||||
state.Tracks.Add(track);
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
internal static AbstractDownloadState? UriToState(Uri directURL)
|
||||
{
|
||||
if (directURL == null || !directURL.Host.EndsWith("youtube.com"))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = HttpUtility.ParseQueryString(directURL.Query)["v"];
|
||||
return key != null ? new State(key) : null;
|
||||
}
|
||||
|
||||
public async Task Prepare()
|
||||
{
|
||||
}
|
||||
|
||||
[JsonName("YouTubeDownloader")]
|
||||
public class State : AbstractDownloadState
|
||||
{
|
||||
public string Key { get; }
|
||||
|
||||
public List<Track> Tracks { get; set; } = new List<Track>();
|
||||
|
||||
[JsonIgnore]
|
||||
public override object[] PrimaryKey => new object[] {Key};
|
||||
|
||||
public State(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
[JsonName("YouTubeTrack")]
|
||||
public class Track
|
||||
{
|
||||
public enum FormatEnum
|
||||
{
|
||||
XWM,
|
||||
WAV
|
||||
}
|
||||
public FormatEnum Format { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public TimeSpan Start { get; set; }
|
||||
|
||||
public TimeSpan End { get; set; }
|
||||
}
|
||||
|
||||
public override bool IsWhitelisted(ServerWhitelist whitelist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var queue = new WorkQueue();
|
||||
await using var folder = await TempFolder.Create();
|
||||
folder.Dir.Combine("tracks").CreateDirectory();
|
||||
var client = new YoutubeClient(Wabbajack.Lib.Http.ClientFactory.Client);
|
||||
var meta = await client.Videos.GetAsync(Key);
|
||||
var video = await client.Videos.Streams.GetManifestAsync(Key);
|
||||
var stream = video.Streams.OfType<AudioOnlyStreamInfo>().Where(f => f.AudioCodec.StartsWith("mp4a")).OrderByDescending(a => a.Bitrate)
|
||||
.ToArray().First();
|
||||
|
||||
var initialDownload = folder.Dir.Combine("initial_download");
|
||||
|
||||
var trackFolder = folder.Dir.Combine("tracks");
|
||||
|
||||
await using (var fs = await initialDownload.Create())
|
||||
{
|
||||
await client.Videos.Streams.CopyToAsync(stream, fs, new Progress($"Downloading {a.Name}"),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
await Tracks.PMap(queue, async track =>
|
||||
{
|
||||
Utils.Status($"Extracting track {track.Name}");
|
||||
await ExtractTrack(initialDownload, trackFolder, track);
|
||||
});
|
||||
|
||||
await using var dest = await destination.Create();
|
||||
using var ar = new ZipArchive(dest, ZipArchiveMode.Create);
|
||||
foreach (var track in trackFolder.EnumerateFiles().OrderBy(e => e))
|
||||
{
|
||||
Utils.Status($"Adding {track.FileName} to archive");
|
||||
var entry = ar.CreateEntry(Path.Combine("Data", "Music", (string)track.RelativeTo(trackFolder)), CompressionLevel.NoCompression);
|
||||
entry.LastWriteTime = meta.UploadDate;
|
||||
await using var es = entry.Open();
|
||||
await using var ins = await track.OpenRead();
|
||||
await ins.CopyToAsync(es);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (VideoUnavailableException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private AbsolutePath FFMpegPath => "Downloaders/Converters/ffmpeg.exe".RelativeTo(AbsolutePath.EntryPoint);
|
||||
private AbsolutePath xWMAEncodePath = "Downloaders/Converters/xWMAEncode.exe".RelativeTo(AbsolutePath.EntryPoint);
|
||||
private Extension WAVExtension = new Extension(".wav");
|
||||
private Extension XWMExtension = new Extension(".xwm");
|
||||
private async Task ExtractTrack(AbsolutePath source, AbsolutePath destFolder, Track track)
|
||||
{
|
||||
Utils.Log($"Extracting {track.Name}");
|
||||
var wavFile = track.Name.RelativeTo(destFolder).WithExtension(WAVExtension);
|
||||
var process = new ProcessHelper
|
||||
{
|
||||
Path = FFMpegPath,
|
||||
Arguments = new object[] {"-hide_banner", "-loglevel", "panic", "-threads", 1, "-i", source, "-ss", track.Start, "-t", track.End - track.Start, wavFile},
|
||||
ThrowOnNonZeroExitCode = true
|
||||
};
|
||||
|
||||
var ffmpegLogs = process.Output.Where(arg => arg.Type == ProcessHelper.StreamType.Output)
|
||||
.ForEachAsync(val =>
|
||||
{
|
||||
Utils.Status($"Extracting {track.Name} - {val.Line}");
|
||||
});
|
||||
|
||||
await process.Start();
|
||||
ffmpegLogs.Dispose();
|
||||
|
||||
if (track.Format == Track.FormatEnum.WAV) return;
|
||||
|
||||
process = new ProcessHelper()
|
||||
{
|
||||
Path = xWMAEncodePath,
|
||||
Arguments = new object[] {"-b", 192000, wavFile, wavFile.ReplaceExtension(XWMExtension)},
|
||||
ThrowOnNonZeroExitCode = true
|
||||
};
|
||||
|
||||
var xwmLogs = process.Output.Where(arg => arg.Type == ProcessHelper.StreamType.Output)
|
||||
.ForEachAsync(val =>
|
||||
{
|
||||
Utils.Log($"Encoding {track.Name} - {val.Line}");
|
||||
});
|
||||
|
||||
await process.Start();
|
||||
xwmLogs.Dispose();
|
||||
|
||||
await wavFile.DeleteAsync();
|
||||
}
|
||||
|
||||
private class Progress : IProgress<double>
|
||||
{
|
||||
private string _prefix;
|
||||
|
||||
public Progress(string prefix)
|
||||
{
|
||||
_prefix = prefix;
|
||||
}
|
||||
public void Report(double value)
|
||||
{
|
||||
Utils.Status(_prefix, Percent.FactoryPutInRange(value));
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<bool> Verify(Archive archive)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new YoutubeClient(Wabbajack.Lib.Http.ClientFactory.Client);
|
||||
var video = await client.Videos.GetAsync(Key);
|
||||
return true;
|
||||
}
|
||||
catch (VideoUnavailableException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public override IDownloader GetDownloader()
|
||||
{
|
||||
return DownloadDispatcher.GetInstance<YouTubeDownloader>();
|
||||
}
|
||||
|
||||
public override string GetManifestURL(Archive a)
|
||||
{
|
||||
return $"https://www.youtube.com/watch?v={Key}";
|
||||
}
|
||||
|
||||
public override string[] GetMetaIni()
|
||||
{
|
||||
IEnumerable<string> start = new List<string> {"[General]", $"directURL=https://www.youtube.com/watch?v={Key}"};
|
||||
start = start.Concat(Tracks.SelectMany((track, idx) =>
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
$"\n[track/{idx}]", $"name={track.Name}", $"start={track.Start}", $"end={track.End}",
|
||||
$"format={track.Format}"
|
||||
};
|
||||
|
||||
}));
|
||||
return start.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -102,8 +102,4 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Downloaders\BethesdaNet" />
|
||||
<Folder Include="Downloaders\Converters" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -485,63 +485,6 @@ namespace Wabbajack.Test
|
||||
Consts.TestMode = true;
|
||||
}
|
||||
|
||||
/* Disabled, will be removed in the future
|
||||
[Fact]
|
||||
public async Task BethesdaNetDownload()
|
||||
{
|
||||
|
||||
var downloader = DownloadDispatcher.GetInstance<BethesdaNetDownloader>();
|
||||
Assert.True(await downloader.IsLoggedIn.FirstAsync());
|
||||
|
||||
var ini = $@"[General]
|
||||
directURL=https://bethesda.net/en/mods/skyrim/mod-detail/4145641";
|
||||
|
||||
await using var filename = new TempFile();
|
||||
var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
|
||||
Assert.NotNull(state);
|
||||
|
||||
var converted = state.ViaJSON();
|
||||
Assert.True(await converted.Verify(new Archive(state: null!) { Name = "mod.ckm"}));
|
||||
|
||||
Assert.True(converted.IsWhitelisted(new ServerWhitelist { AllowedPrefixes = new List<string>() }));
|
||||
|
||||
await converted.Download(new Archive(state: null!) { Name = "mod.zip" }, filename.Path);
|
||||
|
||||
await using var fs = await filename.Path.OpenRead();
|
||||
using var archive = new ZipArchive(fs);
|
||||
var entries = archive.Entries.Select(e => e.FullName).ToList();
|
||||
Assert.Equal(entries, new List<string> {@"Data\TestCK.esp", @"Data\TestCK.ini"});
|
||||
}*/
|
||||
|
||||
/*
|
||||
[Fact]
|
||||
public async Task YoutubeDownloader()
|
||||
{
|
||||
|
||||
var infered_ini = await DownloadDispatcher.Infer(new Uri("https://www.youtube.com/watch?v=4ceowgHn8BE"));
|
||||
Assert.IsAssignableFrom<YouTubeDownloader.State>(infered_ini);
|
||||
Assert.Equal(15, ((YouTubeDownloader.State)infered_ini).Tracks.Count);
|
||||
|
||||
var ini = string.Join("\n", infered_ini.GetMetaIni());
|
||||
|
||||
var state = (YouTubeDownloader.State)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
|
||||
Assert.Equal(15, state.Tracks.Count);
|
||||
Assert.NotNull(state);
|
||||
|
||||
|
||||
|
||||
var converted = RoundTripState(state);
|
||||
Assert.True(await converted.Verify(new Archive(state: null!) { Name = "yt_test.zip"}));
|
||||
|
||||
Assert.True(converted.IsWhitelisted(new ServerWhitelist { AllowedPrefixes = new List<string>() }));
|
||||
|
||||
await using var tempFile = new TempFile();
|
||||
await converted.Download(new Archive(state: null!) { Name = "yt_test.zip"}, tempFile.Path);
|
||||
Assert.Equal(Hash.FromBase64("pD7UoVNY4o8="), await tempFile.Path.FileHashAsync());
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests that files from different sources don't overwrite eachother when downloaded by AInstaller
|
||||
/// </summary>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 714 KiB |
Binary file not shown.
Before Width: | Height: | Size: 642 KiB |
@ -1,64 +0,0 @@
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using CefSharp.Wpf;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
using Wabbajack.Lib.WebAutomation;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public class BethesdaNetLoginVM : ViewModel, IBackNavigatingVM
|
||||
{
|
||||
[Reactive]
|
||||
public string Instructions { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public ViewModel NavigateBackTarget { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> LoginViaSkyrimSE { get; }
|
||||
public ReactiveCommand<Unit, Unit> LoginViaFallout4 { get; }
|
||||
|
||||
private Subject<bool> LoggingIn = new Subject<bool>();
|
||||
|
||||
private BethesdaNetLoginVM()
|
||||
{
|
||||
Instructions = "Login to Bethesda.NET in-game...";
|
||||
LoginViaSkyrimSE = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
LoggingIn.OnNext(true);
|
||||
Instructions = "Starting Skyrim Special Edition...";
|
||||
await BethesdaNetDownloader.Login(Game.SkyrimSpecialEdition);
|
||||
LoggingIn.OnNext(false);
|
||||
await BackCommand.Execute();
|
||||
}, Game.SkyrimSpecialEdition.MetaData().IsInstalled
|
||||
? LoggingIn.Select(e => !e).StartWith(true)
|
||||
: Observable.Return(false));
|
||||
|
||||
LoginViaFallout4 = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
LoggingIn.OnNext(true);
|
||||
Instructions = "Starting Fallout 4...";
|
||||
await BethesdaNetDownloader.Login(Game.Fallout4);
|
||||
LoggingIn.OnNext(false);
|
||||
await BackCommand.Execute();
|
||||
}, Game.Fallout4.MetaData().IsInstalled
|
||||
? LoggingIn.Select(e => !e).StartWith(true)
|
||||
: Observable.Return(false));
|
||||
}
|
||||
|
||||
public static async Task<BethesdaNetLoginVM> GetNew()
|
||||
{
|
||||
// Make sure libraries are extracted first
|
||||
return new BethesdaNetLoginVM();
|
||||
}
|
||||
}
|
||||
}
|
@ -57,21 +57,6 @@ namespace Wabbajack
|
||||
MainWindow.NavigateTo(oldPane);
|
||||
}
|
||||
|
||||
private async Task WrapBethesdaNetLogin(IUserIntervention intervention)
|
||||
{
|
||||
CancellationTokenSource cancel = new CancellationTokenSource();
|
||||
var oldPane = MainWindow.ActivePane;
|
||||
var vm = await BethesdaNetLoginVM.GetNew();
|
||||
MainWindow.NavigateTo(vm);
|
||||
vm.BackCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
cancel.Cancel();
|
||||
MainWindow.NavigateTo(oldPane);
|
||||
intervention.Cancel();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public async Task Handle(IUserIntervention msg)
|
||||
{
|
||||
switch (msg)
|
||||
@ -90,9 +75,6 @@ namespace Wabbajack
|
||||
case ManuallyDownloadFile c:
|
||||
await WrapBrowserJob(msg, (vm, cancel) => HandleManualDownload(vm, cancel, c));
|
||||
break;
|
||||
case RequestBethesdaNetLogin c:
|
||||
await WrapBethesdaNetLogin(c);
|
||||
break;
|
||||
case AbstractNeedsLoginDownloader.RequestSiteLogin c:
|
||||
await WrapBrowserJob(msg, async (vm, cancel) =>
|
||||
{
|
||||
|
@ -37,9 +37,6 @@
|
||||
<DataTemplate DataType="{x:Type local:WebBrowserVM}">
|
||||
<local:WebBrowserView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:BethesdaNetLoginVM}">
|
||||
<local:BethesdaNetLoginView />
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:SettingsVM}">
|
||||
<local:SettingsView ViewModel="{Binding}" />
|
||||
</DataTemplate>
|
||||
|
@ -87,8 +87,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\GameGridIcons\Fallout4.png" />
|
||||
<Resource Include="Resources\GameGridIcons\SkyrimSpecialEdition.png" />
|
||||
<Resource Include="Resources\middle_mouse_button.png" />
|
||||
<Resource Include="Resources\MO2Button.png" />
|
||||
<Resource Include="Resources\VortexButton.png" />
|
||||
|
Loading…
Reference in New Issue
Block a user