2020-08-08 18:26:55 +00:00
using System ;
2020-11-05 21:30:00 +00:00
using System.Collections.Generic ;
2020-04-06 20:48:54 +00:00
using System.ComponentModel.DataAnnotations ;
2019-10-12 21:37:16 +00:00
using System.Linq ;
2020-01-07 05:44:05 +00:00
using System.Reactive ;
2019-12-20 20:51:10 +00:00
using System.Reactive.Linq ;
2020-12-31 06:44:42 +00:00
using System.Threading ;
2019-12-06 05:29:17 +00:00
using System.Threading.Tasks ;
2020-06-06 21:44:30 +00:00
using F23.StringSimilarity ;
2020-04-01 12:17:38 +00:00
using Newtonsoft.Json ;
2019-12-20 20:51:10 +00:00
using ReactiveUI ;
2019-10-12 21:37:16 +00:00
using Wabbajack.Common ;
2020-04-06 20:48:54 +00:00
using Wabbajack.Common.Serialization.Json ;
2019-12-04 04:12:08 +00:00
using Wabbajack.Common.StatusFeed.Errors ;
2019-10-16 03:10:34 +00:00
using Wabbajack.Lib.NexusApi ;
using Wabbajack.Lib.Validation ;
2020-03-30 22:26:34 +00:00
using Game = Wabbajack . Common . Game ;
2019-10-12 21:37:16 +00:00
2019-10-16 03:10:34 +00:00
namespace Wabbajack.Lib.Downloaders
2019-10-12 21:37:16 +00:00
{
2020-01-05 01:01:43 +00:00
public class NexusDownloader : IDownloader , INeedsLogin
2019-10-12 21:37:16 +00:00
{
2019-12-14 05:46:20 +00:00
private bool _prepared ;
2020-02-06 05:30:31 +00:00
private AsyncLock _lock = new AsyncLock ( ) ;
2020-04-09 20:20:34 +00:00
private UserStatus ? _status ;
2020-08-12 04:25:12 +00:00
public INexusApi ? Client ;
2019-12-14 05:46:20 +00:00
2019-12-20 20:51:10 +00:00
public IObservable < bool > IsLoggedIn = > Utils . HaveEncryptedJsonObservable ( "nexusapikey" ) ;
public string SiteName = > "Nexus Mods" ;
2020-01-06 04:49:11 +00:00
public IObservable < string > MetaInfo = > Observable . Return ( "" ) ;
2019-12-20 20:51:10 +00:00
public Uri SiteURL = > new Uri ( "https://www.nexusmods.com" ) ;
public Uri IconUri = > new Uri ( "https://www.nexusmods.com/favicon.ico" ) ;
2020-01-05 00:05:31 +00:00
2020-01-07 05:44:05 +00:00
public ReactiveCommand < Unit , Unit > TriggerLogin { get ; }
public ReactiveCommand < Unit , Unit > ClearLogin { get ; }
2019-12-20 20:51:10 +00:00
2020-01-05 00:05:31 +00:00
public NexusDownloader ( )
{
TriggerLogin = ReactiveCommand . CreateFromTask (
execute : ( ) = > Utils . CatchAndLog ( NexusApiClient . RequestAndCacheAPIKey ) ,
2020-01-09 03:22:49 +00:00
canExecute : IsLoggedIn . Select ( b = > ! b ) . ObserveOnGuiThread ( ) ) ;
2020-06-26 17:08:30 +00:00
ClearLogin = ReactiveCommand . CreateFromTask (
execute : ( ) = > Utils . CatchAndLog ( async ( ) = > await Utils . DeleteEncryptedJson ( "nexusapikey" ) ) ,
2020-01-09 03:22:49 +00:00
canExecute : IsLoggedIn . ObserveOnGuiThread ( ) ) ;
2020-01-05 00:05:31 +00:00
}
2020-04-09 20:20:34 +00:00
public async Task < AbstractDownloadState ? > GetDownloaderState ( dynamic archiveINI , bool quickMode )
2019-10-12 21:37:16 +00:00
{
2020-04-09 20:20:34 +00:00
var general = archiveINI . General ;
2019-10-12 21:37:16 +00:00
if ( general . modID ! = null & & general . fileID ! = null & & general . gameName ! = null )
{
2020-04-02 21:46:11 +00:00
var game = GameRegistry . GetByFuzzyName ( ( string ) general . gameName ) . Game ;
2020-07-06 06:58:13 +00:00
2020-04-03 03:57:59 +00:00
if ( quickMode )
{
return new State
{
Game = GameRegistry . GetByFuzzyName ( ( string ) general . gameName ) . Game ,
ModID = long . Parse ( general . modID ) ,
FileID = long . Parse ( general . fileID ) ,
} ;
}
2019-12-07 03:50:50 +00:00
var client = await NexusApiClient . Get ( ) ;
2020-04-02 21:46:11 +00:00
ModInfo info ;
2020-02-04 02:06:35 +00:00
try
{
2020-04-02 21:46:11 +00:00
info = await client . GetModInfo ( game , long . Parse ( ( string ) general . modID ) ) ;
2020-02-04 02:06:35 +00:00
}
catch ( Exception )
{
Utils . Error ( $"Error getting mod info for Nexus mod with {general.modID}" ) ;
throw ;
}
2020-03-04 12:10:49 +00:00
2020-05-13 23:46:24 +00:00
try
{
return new State
{
Name = NexusApiUtils . FixupSummary ( info . name ) ,
Author = NexusApiUtils . FixupSummary ( info . author ) ,
Version = general . version ? ? "0.0.0.0" ,
ImageURL = info . picture_url ,
IsNSFW = info . contains_adult_content ,
Description = NexusApiUtils . FixupSummary ( info . summary ) ,
Game = GameRegistry . GetByFuzzyName ( ( string ) general . gameName ) . Game ,
ModID = long . Parse ( general . modID ) ,
FileID = long . Parse ( general . fileID )
} ;
}
catch ( FormatException )
2019-10-12 21:37:16 +00:00
{
2020-05-13 23:46:24 +00:00
Utils . Log (
$"Cannot parse ModID/FileID from {(string)general.gameName} {(string)general.modID} {(string)general.fileID}" ) ;
throw ;
}
2019-10-12 21:37:16 +00:00
}
return null ;
}
2019-12-07 02:45:13 +00:00
public async Task Prepare ( )
2019-10-12 22:15:20 +00:00
{
2019-12-14 05:46:20 +00:00
if ( ! _prepared )
2019-10-12 22:15:20 +00:00
{
2020-04-17 03:52:19 +00:00
using var _ = await _lock . WaitAsync ( ) ;
2020-02-06 05:30:31 +00:00
// Could have become prepared while we waited for the lock
if ( ! _prepared )
2019-12-14 05:46:20 +00:00
{
2020-08-08 12:56:43 +00:00
if ( CLIArguments . ApiKey ! = null )
{
await CLIArguments . ApiKey . ToEcryptedJson ( "nexusapikey" ) ;
}
2020-08-12 04:25:12 +00:00
Client = await NexusApiClient . Get ( ) ;
_status = await Client . GetUserStatus ( ) ;
if ( ! Client . IsAuthenticated )
2020-02-06 05:30:31 +00:00
{
Utils . ErrorThrow ( new UnconvertedError (
$"Authenticating for the Nexus failed. A nexus account is required to automatically download mods." ) ) ;
return ;
}
2020-05-16 15:08:40 +00:00
2020-08-08 18:26:55 +00:00
/ * Disabled for better User experience
2020-02-06 05:30:31 +00:00
if ( ! await _client . IsPremium ( ) )
2019-12-14 05:46:20 +00:00
{
2020-02-06 05:30:31 +00:00
var result = await Utils . Log ( new YesNoIntervention (
"Wabbajack can operate without a premium account, but downloads will be slower and the install process will require more user interactions (you will have to start each download by hand). Are you sure you wish to continue?" ,
"Continue without Premium?" ) ) . Task ;
if ( result = = ConfirmationIntervention . Choice . Abort )
2019-12-14 05:46:20 +00:00
{
2020-02-06 05:30:31 +00:00
Utils . ErrorThrow ( new UnconvertedError ( $"Aborting at the request of the user" ) ) ;
2019-12-14 05:46:20 +00:00
}
}
2020-08-08 18:26:55 +00:00
* /
2020-02-26 05:05:33 +00:00
_prepared = true ;
2019-12-14 05:46:20 +00:00
}
2019-10-12 22:15:20 +00:00
}
}
2020-11-05 21:30:00 +00:00
public async Task < bool > HaveEnoughAPICalls ( IEnumerable < Archive > archives )
{
if ( await Client ! . IsPremium ( ) )
return true ;
var count = archives . Select ( a = > a . State ) . OfType < State > ( ) . Count ( ) ;
return count < Client ! . RemainingAPICalls ;
}
2020-04-06 20:48:54 +00:00
[JsonName("NexusDownloader")]
2020-05-20 03:25:41 +00:00
public class State : AbstractDownloadState , IMetaState , IUpgradingState
2019-10-12 21:37:16 +00:00
{
2020-04-06 20:48:54 +00:00
[JsonIgnore]
2020-03-30 22:26:34 +00:00
public Uri URL = > new Uri ( $"http://nexusmods.com/{Game.MetaData().NexusName}/mods/{ModID}" ) ;
2020-03-04 12:10:49 +00:00
2020-04-09 20:20:34 +00:00
public string? Name { get ; set ; }
2020-03-04 12:10:49 +00:00
2020-04-09 20:20:34 +00:00
public string? Author { get ; set ; }
2020-03-04 12:10:49 +00:00
2020-04-09 20:20:34 +00:00
public string? Version { get ; set ; }
2020-03-04 12:10:49 +00:00
2020-04-15 12:05:05 +00:00
public Uri ? ImageURL { get ; set ; }
2020-03-04 12:10:49 +00:00
public bool IsNSFW { get ; set ; }
2020-04-09 20:20:34 +00:00
public string? Description { get ; set ; }
2020-03-04 12:10:49 +00:00
2020-04-01 12:17:38 +00:00
[JsonProperty("GameName")]
[JsonConverter(typeof(Utils.GameConverter))]
2020-03-30 22:26:34 +00:00
public Game Game { get ; set ; }
2020-03-27 03:10:23 +00:00
2020-04-03 03:57:59 +00:00
public long ModID { get ; set ; }
public long FileID { get ; set ; }
2020-03-27 03:10:23 +00:00
2020-03-04 12:10:49 +00:00
public async Task < bool > LoadMetaData ( )
{
return true ;
}
2020-03-27 03:10:23 +00:00
2020-04-06 20:48:54 +00:00
[JsonIgnore]
2020-03-30 22:26:34 +00:00
public override object [ ] PrimaryKey { get = > new object [ ] { Game , ModID , FileID } ; }
2019-10-12 21:37:16 +00:00
public override bool IsWhitelisted ( ServerWhitelist whitelist )
{
// Nexus files are always whitelisted
return true ;
}
2020-03-25 22:30:43 +00:00
public override async Task < bool > Download ( Archive a , AbsolutePath destination )
2019-10-12 21:37:16 +00:00
{
string url ;
try
{
2019-12-07 03:50:50 +00:00
var client = await NexusApiClient . Get ( ) ;
2020-01-02 00:11:13 +00:00
url = await client . GetNexusDownloadLink ( this ) ;
2019-10-12 21:37:16 +00:00
}
2020-11-14 14:21:13 +00:00
catch ( NexusAPIQuotaExceeded ex )
{
Utils . Log ( ex . ExtendedDescription ) ;
throw ;
}
2020-11-14 14:26:04 +00:00
catch ( Exception )
2019-10-12 21:37:16 +00:00
{
2020-01-18 19:57:26 +00:00
return false ;
2019-10-12 21:37:16 +00:00
}
2020-04-09 20:20:34 +00:00
return await new HTTPDownloader . State ( url ) . Download ( a , destination ) ;
2019-10-12 21:37:16 +00:00
}
2020-12-31 06:44:42 +00:00
public override async Task < bool > Verify ( Archive a , CancellationToken ? token = null )
2019-10-12 21:37:16 +00:00
{
try
{
2019-12-07 03:50:50 +00:00
var client = await NexusApiClient . Get ( ) ;
2020-08-12 04:25:12 +00:00
var modInfo = await client . GetModInfo ( Game , ModID ) ;
if ( ! modInfo . available ) return false ;
2020-04-03 03:57:59 +00:00
var modFiles = await client . GetModFiles ( Game , ModID ) ;
2019-12-01 14:44:01 +00:00
var found = modFiles . files
2020-04-03 03:57:59 +00:00
. FirstOrDefault ( file = > file . file_id = = FileID & & file . category_name ! = null ) ;
2020-05-02 13:39:07 +00:00
2019-11-21 05:57:48 +00:00
return found ! = null ;
2019-10-12 21:37:16 +00:00
}
catch ( Exception ex )
{
2020-03-30 22:26:34 +00:00
Utils . Log ( $"{Name} - {Game} - {ModID} - {FileID} - Error getting Nexus download URL - {ex}" ) ;
2019-10-12 21:37:16 +00:00
return false ;
}
}
2019-10-12 22:15:20 +00:00
public override IDownloader GetDownloader ( )
{
return DownloadDispatcher . GetInstance < NexusDownloader > ( ) ;
}
2019-10-12 22:54:25 +00:00
2020-02-02 12:23:07 +00:00
public override string GetManifestURL ( Archive a )
2020-02-02 12:15:29 +00:00
{
2020-03-30 22:26:34 +00:00
return $"http://nexusmods.com/{Game.MetaData().NexusName}/mods/{ModID}" ;
2020-02-02 12:15:29 +00:00
}
2020-01-11 04:15:53 +00:00
public override string [ ] GetMetaIni ( )
{
2020-04-03 03:57:59 +00:00
return new [ ] { "[General]" , $"gameName={Game.MetaData().MO2ArchiveName}" , $"modID={ModID}" , $"fileID={FileID}" } ;
2020-01-11 04:15:53 +00:00
}
2020-05-20 03:25:41 +00:00
2020-08-17 04:15:19 +00:00
public override async Task < ( Archive ? Archive , TempFile NewFile ) > FindUpgrade ( Archive a , Func < Archive , Task < AbsolutePath > > downloadResolver )
2020-05-20 03:25:41 +00:00
{
2020-11-16 00:40:49 +00:00
var client = DownloadDispatcher . GetInstance < NexusDownloader > ( ) . Client ? ? await NexusApiClient . Get ( ) ;
await client . IsPremium ( ) ;
2020-11-14 14:21:13 +00:00
if ( client . RemainingAPICalls < = 0 )
throw new NexusAPIQuotaExceeded ( ) ;
2020-05-20 03:25:41 +00:00
var mod = await client . GetModInfo ( Game , ModID ) ;
2020-08-12 04:25:12 +00:00
if ( ! mod . available )
return default ;
2020-05-20 03:25:41 +00:00
var files = await client . GetModFiles ( Game , ModID ) ;
var oldFile = files . files . FirstOrDefault ( f = > f . file_id = = FileID ) ;
2020-06-06 21:44:30 +00:00
var nl = new Levenshtein ( ) ;
var newFile = files . files . Where ( f = > f . category_name ! = null )
2021-01-01 00:06:56 +00:00
. OrderBy ( f = > nl . Distance ( oldFile ! . name . ToLowerInvariant ( ) , f . name . ToLowerInvariant ( ) ) ) . FirstOrDefault ( ) ;
2020-05-20 03:25:41 +00:00
if ( ! mod . available | | oldFile = = default | | newFile = = default )
{
return default ;
}
2020-05-23 21:03:25 +00:00
// Size is in KB
2020-06-06 21:44:30 +00:00
if ( oldFile . size > 4_500_000 | | newFile . size > 4_500_000 | | oldFile . file_id = = newFile . file_id )
2020-05-23 21:03:25 +00:00
{
return default ;
}
2020-05-20 03:25:41 +00:00
2020-05-21 21:25:44 +00:00
var newArchive = new Archive ( new State { Game = Game , ModID = ModID , FileID = newFile . file_id } )
2020-05-20 03:25:41 +00:00
{
Name = newFile . file_name ,
} ;
2020-08-12 22:23:02 +00:00
var fastPath = await downloadResolver ( newArchive ) ;
2020-08-12 04:25:12 +00:00
if ( fastPath ! = default )
{
newArchive . Size = fastPath . Size ;
newArchive . Hash = await fastPath . FileHashAsync ( ) ;
return ( newArchive , new TempFile ( ) ) ;
}
2020-11-11 13:25:57 +00:00
Utils . Log ( $"Downloading possible upgrade {newArchive.State.PrimaryKeyString}" ) ;
2020-08-12 04:25:12 +00:00
var tempFile = new TempFile ( ) ;
2020-05-20 03:25:41 +00:00
await newArchive . State . Download ( newArchive , tempFile . Path ) ;
newArchive . Size = tempFile . Path . Size ;
newArchive . Hash = await tempFile . Path . FileHashAsync ( ) ;
2020-11-11 13:25:57 +00:00
Utils . Log ( $"Possible upgrade {newArchive.State.PrimaryKeyString} downloaded" ) ;
2020-05-20 03:25:41 +00:00
return ( newArchive , tempFile ) ;
}
2020-10-01 03:50:09 +00:00
public override async Task < bool > ValidateUpgrade ( Hash srcHash , AbstractDownloadState newArchiveState )
2020-05-20 03:25:41 +00:00
{
var state = ( State ) newArchiveState ;
return Game = = state . Game & & ModID = = state . ModID ;
}
2019-10-12 21:37:16 +00:00
}
}
}