2019-11-29 04:32:51 +00:00
using JsonPrettyPrinterPlus ;
using LibHac ;
2019-09-02 16:03:57 +00:00
using LibHac.Fs ;
2019-10-17 06:17:44 +00:00
using LibHac.FsSystem ;
using LibHac.FsSystem.NcaUtils ;
using LibHac.Spl ;
2019-09-02 16:03:57 +00:00
using Ryujinx.Common.Logging ;
2019-11-29 04:32:51 +00:00
using Ryujinx.HLE.FileSystem ;
using Ryujinx.HLE.Loaders.Npdm ;
2019-09-02 16:03:57 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using System.Text ;
2019-11-29 04:32:51 +00:00
using Utf8Json ;
using Utf8Json.Resolvers ;
2019-10-17 06:17:44 +00:00
2019-11-29 04:32:51 +00:00
using TitleLanguage = Ryujinx . HLE . HOS . SystemState . TitleLanguage ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
namespace Ryujinx.Ui
2019-09-02 16:03:57 +00:00
{
public class ApplicationLibrary
{
2019-11-29 04:32:51 +00:00
public static event EventHandler < ApplicationAddedEventArgs > ApplicationAdded ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
private static readonly byte [ ] _nspIcon = GetResourceBytes ( "Ryujinx.Ui.assets.NSPIcon.png" ) ;
private static readonly byte [ ] _xciIcon = GetResourceBytes ( "Ryujinx.Ui.assets.XCIIcon.png" ) ;
private static readonly byte [ ] _ncaIcon = GetResourceBytes ( "Ryujinx.Ui.assets.NCAIcon.png" ) ;
private static readonly byte [ ] _nroIcon = GetResourceBytes ( "Ryujinx.Ui.assets.NROIcon.png" ) ;
private static readonly byte [ ] _nsoIcon = GetResourceBytes ( "Ryujinx.Ui.assets.NSOIcon.png" ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
private static Keyset _keySet ;
private static TitleLanguage _desiredTitleLanguage ;
private static ApplicationMetadata _appMetadata ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
public static void LoadApplications ( List < string > appDirs , Keyset keySet , TitleLanguage desiredTitleLanguage )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
int numApplicationsFound = 0 ;
int numApplicationsLoaded = 0 ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
_keySet = keySet ;
_desiredTitleLanguage = desiredTitleLanguage ;
2019-09-02 16:03:57 +00:00
// Builds the applications list with paths to found applications
List < string > applications = new List < string > ( ) ;
2019-11-29 04:32:51 +00:00
foreach ( string appDir in appDirs )
2019-09-02 16:03:57 +00:00
{
if ( Directory . Exists ( appDir ) = = false )
{
Logger . PrintWarning ( LogClass . Application , $"The \" game_dirs \ " section in \"Config.json\" contains an invalid directory: \"{appDir}\"" ) ;
continue ;
}
2019-11-29 04:32:51 +00:00
foreach ( string app in Directory . GetFiles ( appDir , "*.*" , SearchOption . AllDirectories ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
if ( ( Path . GetExtension ( app ) = = ".xci" ) | |
( Path . GetExtension ( app ) = = ".nro" ) | |
( Path . GetExtension ( app ) = = ".nso" ) | |
( Path . GetFileName ( app ) = = "hbl.nsp" ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
applications . Add ( app ) ;
numApplicationsFound + + ;
}
else if ( ( Path . GetExtension ( app ) = = ".nsp" ) | | ( Path . GetExtension ( app ) = = ".pfs0" ) )
{
try
{
bool hasMainNca = false ;
PartitionFileSystem nsp = new PartitionFileSystem ( new FileStream ( app , FileMode . Open , FileAccess . Read ) . AsStorage ( ) ) ;
foreach ( DirectoryEntryEx fileEntry in nsp . EnumerateEntries ( "/" , "*.nca" ) )
{
nsp . OpenFile ( out IFile ncaFile , fileEntry . FullPath , OpenMode . Read ) . ThrowIfFailure ( ) ;
Nca nca = new Nca ( _keySet , ncaFile . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
if ( nca . Header . ContentType = = NcaContentType . Program & & ! nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
{
hasMainNca = true ;
}
}
if ( ! hasMainNca )
{
continue ;
}
}
catch ( InvalidDataException )
{
Logger . PrintWarning ( LogClass . Application , $"{app}: The header key is incorrect or missing and therefore the NCA header content type check has failed." ) ;
}
applications . Add ( app ) ;
numApplicationsFound + + ;
}
else if ( Path . GetExtension ( app ) = = ".nca" )
{
try
{
Nca nca = new Nca ( _keySet , new FileStream ( app , FileMode . Open , FileAccess . Read ) . AsStorage ( ) ) ;
int dataIndex = Nca . GetSectionIndexFromType ( NcaSectionType . Data , NcaContentType . Program ) ;
if ( nca . Header . ContentType ! = NcaContentType . Program | | nca . Header . GetFsHeader ( dataIndex ) . IsPatchSection ( ) )
{
continue ;
}
}
catch ( InvalidDataException )
{
Logger . PrintWarning ( LogClass . Application , $"{app}: The header key is incorrect or missing and therefore the NCA header content type check has failed." ) ;
}
applications . Add ( app ) ;
numApplicationsFound + + ;
2019-09-02 16:03:57 +00:00
}
}
}
2019-11-29 04:32:51 +00:00
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
2019-09-02 16:03:57 +00:00
foreach ( string applicationPath in applications )
{
2019-11-29 04:32:51 +00:00
double fileSize = new FileInfo ( applicationPath ) . Length * 0.000000000931 ;
string titleName = "Unknown" ;
string titleId = "0000000000000000" ;
string developer = "Unknown" ;
string version = "0" ;
2019-09-02 16:03:57 +00:00
byte [ ] applicationIcon = null ;
using ( FileStream file = new FileStream ( applicationPath , FileMode . Open , FileAccess . Read ) )
{
if ( ( Path . GetExtension ( applicationPath ) = = ".nsp" ) | |
( Path . GetExtension ( applicationPath ) = = ".pfs0" ) | |
( Path . GetExtension ( applicationPath ) = = ".xci" ) )
{
try
{
2019-11-29 04:32:51 +00:00
PartitionFileSystem pfs ;
2019-09-02 16:03:57 +00:00
if ( Path . GetExtension ( applicationPath ) = = ".xci" )
{
2019-11-29 04:32:51 +00:00
Xci xci = new Xci ( _keySet , file . AsStorage ( ) ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
pfs = xci . OpenPartition ( XciPartitionType . Secure ) ;
2019-09-02 16:03:57 +00:00
}
else
{
2019-11-29 04:32:51 +00:00
pfs = new PartitionFileSystem ( file . AsStorage ( ) ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
// Store the ControlFS in variable called controlFs
IFileSystem controlFs = GetControlFs ( pfs ) ;
2019-10-17 06:17:44 +00:00
2019-11-29 04:32:51 +00:00
// If this is null then this is probably not a normal NSP, it's probably an ExeFS as an NSP
if ( controlFs = = null )
{
applicationIcon = _nspIcon ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
Result result = pfs . OpenFile ( out IFile npdmFile , "/main.npdm" , OpenMode . Read ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
if ( result ! = ResultFs . PathNotFound )
{
Npdm npdm = new Npdm ( npdmFile . AsStream ( ) ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
titleName = npdm . TitleName ;
titleId = npdm . Aci0 . TitleId . ToString ( "x16" ) ;
}
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
else
{
// Creates NACP class from the NACP file
controlFs . OpenFile ( out IFile controlNacpFile , "/control.nacp" , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
Nacp controlData = new Nacp ( controlNacpFile . AsStream ( ) ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
// Get the title name, title ID, developer name and version number from the NACP
version = controlData . DisplayVersion ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
titleName = controlData . Descriptions [ ( int ) _desiredTitleLanguage ] . Title ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
if ( string . IsNullOrWhiteSpace ( titleName ) )
{
titleName = controlData . Descriptions . ToList ( ) . Find ( x = > ! string . IsNullOrWhiteSpace ( x . Title ) ) . Title ;
}
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
titleId = controlData . PresenceGroupId . ToString ( "x16" ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
if ( string . IsNullOrWhiteSpace ( titleId ) )
{
titleId = controlData . SaveDataOwnerId . ToString ( "x16" ) ;
}
2019-10-17 06:17:44 +00:00
2019-11-29 04:32:51 +00:00
if ( string . IsNullOrWhiteSpace ( titleId ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
titleId = ( controlData . AddOnContentBaseId - 0x1000 ) . ToString ( "x16" ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
developer = controlData . Descriptions [ ( int ) _desiredTitleLanguage ] . Developer ;
if ( string . IsNullOrWhiteSpace ( developer ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
developer = controlData . Descriptions . ToList ( ) . Find ( x = > ! string . IsNullOrWhiteSpace ( x . Developer ) ) . Developer ;
}
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
// Read the icon from the ControlFS and store it as a byte array
try
{
controlFs . OpenFile ( out IFile icon , $"/icon_{_desiredTitleLanguage}.dat" , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-10-17 06:17:44 +00:00
2019-09-02 16:03:57 +00:00
using ( MemoryStream stream = new MemoryStream ( ) )
{
icon . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
2019-11-29 04:32:51 +00:00
}
catch ( HorizonResultException )
{
foreach ( DirectoryEntryEx entry in controlFs . EnumerateEntries ( "/" , "*" ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
if ( entry . Name = = "control.nacp" )
{
continue ;
}
controlFs . OpenFile ( out IFile icon , entry . FullPath , OpenMode . Read ) . ThrowIfFailure ( ) ;
using ( MemoryStream stream = new MemoryStream ( ) )
{
icon . AsStream ( ) . CopyTo ( stream ) ;
applicationIcon = stream . ToArray ( ) ;
}
if ( applicationIcon ! = null )
{
break ;
}
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
if ( applicationIcon = = null )
{
applicationIcon = Path . GetExtension ( applicationPath ) = = ".xci" ? _xciIcon : _nspIcon ;
}
2019-09-02 16:03:57 +00:00
}
}
}
catch ( MissingKeyException exception )
{
2019-11-29 04:32:51 +00:00
applicationIcon = Path . GetExtension ( applicationPath ) = = ".xci" ? _xciIcon : _nspIcon ;
2019-09-02 16:03:57 +00:00
Logger . PrintWarning ( LogClass . Application , $"Your key set is missing a key with the name: {exception.Name}" ) ;
}
catch ( InvalidDataException )
{
2019-11-29 04:32:51 +00:00
applicationIcon = Path . GetExtension ( applicationPath ) = = ".xci" ? _xciIcon : _nspIcon ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
Logger . PrintWarning ( LogClass . Application , $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}" ) ;
2019-09-02 16:03:57 +00:00
}
}
else if ( Path . GetExtension ( applicationPath ) = = ".nro" )
{
BinaryReader reader = new BinaryReader ( file ) ;
2019-11-29 04:32:51 +00:00
byte [ ] Read ( long position , int size )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
file . Seek ( position , SeekOrigin . Begin ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
return reader . ReadBytes ( size ) ;
2019-09-02 16:03:57 +00:00
}
file . Seek ( 24 , SeekOrigin . Begin ) ;
2019-11-29 04:32:51 +00:00
int assetOffset = reader . ReadInt32 ( ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
if ( Encoding . ASCII . GetString ( Read ( assetOffset , 4 ) ) = = "ASET" )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
byte [ ] iconSectionInfo = Read ( assetOffset + 8 , 0x10 ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
long iconOffset = BitConverter . ToInt64 ( iconSectionInfo , 0 ) ;
long iconSize = BitConverter . ToInt64 ( iconSectionInfo , 8 ) ;
2019-09-02 16:03:57 +00:00
ulong nacpOffset = reader . ReadUInt64 ( ) ;
ulong nacpSize = reader . ReadUInt64 ( ) ;
// Reads and stores game icon as byte array
2019-11-29 04:32:51 +00:00
applicationIcon = Read ( assetOffset + iconOffset , ( int ) iconSize ) ;
2019-09-02 16:03:57 +00:00
// Creates memory stream out of byte array which is the NACP
2019-11-29 04:32:51 +00:00
using ( MemoryStream stream = new MemoryStream ( Read ( assetOffset + ( int ) nacpOffset , ( int ) nacpSize ) ) )
2019-09-02 16:03:57 +00:00
{
// Creates NACP class from the memory stream
Nacp controlData = new Nacp ( stream ) ;
// Get the title name, title ID, developer name and version number from the NACP
version = controlData . DisplayVersion ;
2019-11-29 04:32:51 +00:00
titleName = controlData . Descriptions [ ( int ) _desiredTitleLanguage ] . Title ;
2019-09-02 16:03:57 +00:00
if ( string . IsNullOrWhiteSpace ( titleName ) )
{
titleName = controlData . Descriptions . ToList ( ) . Find ( x = > ! string . IsNullOrWhiteSpace ( x . Title ) ) . Title ;
}
titleId = controlData . PresenceGroupId . ToString ( "x16" ) ;
if ( string . IsNullOrWhiteSpace ( titleId ) )
{
titleId = controlData . SaveDataOwnerId . ToString ( "x16" ) ;
}
if ( string . IsNullOrWhiteSpace ( titleId ) )
{
titleId = ( controlData . AddOnContentBaseId - 0x1000 ) . ToString ( "x16" ) ;
}
2019-11-29 04:32:51 +00:00
developer = controlData . Descriptions [ ( int ) _desiredTitleLanguage ] . Developer ;
2019-09-02 16:03:57 +00:00
if ( string . IsNullOrWhiteSpace ( developer ) )
{
developer = controlData . Descriptions . ToList ( ) . Find ( x = > ! string . IsNullOrWhiteSpace ( x . Developer ) ) . Developer ;
}
}
}
else
{
2019-11-29 04:32:51 +00:00
applicationIcon = _nroIcon ;
2019-09-02 16:03:57 +00:00
}
}
// If its an NCA or NSO we just set defaults
else if ( ( Path . GetExtension ( applicationPath ) = = ".nca" ) | | ( Path . GetExtension ( applicationPath ) = = ".nso" ) )
{
2019-11-29 04:32:51 +00:00
applicationIcon = Path . GetExtension ( applicationPath ) = = ".nca" ? _ncaIcon : _nsoIcon ;
titleName = Path . GetFileNameWithoutExtension ( applicationPath ) ;
2019-09-02 16:03:57 +00:00
}
}
2019-11-29 04:32:51 +00:00
( bool favorite , string timePlayed , string lastPlayed ) = GetMetadata ( titleId ) ;
2019-09-02 16:03:57 +00:00
ApplicationData data = new ApplicationData ( )
{
2019-11-29 04:32:51 +00:00
Favorite = favorite ,
Icon = applicationIcon ,
TitleName = titleName ,
TitleId = titleId ,
Developer = developer ,
Version = version ,
TimePlayed = timePlayed ,
LastPlayed = lastPlayed ,
FileExtension = Path . GetExtension ( applicationPath ) . ToUpper ( ) . Remove ( 0 , 1 ) ,
FileSize = ( fileSize < 1 ) ? ( fileSize * 1024 ) . ToString ( "0.##" ) + "MB" : fileSize . ToString ( "0.##" ) + "GB" ,
Path = applicationPath ,
2019-09-02 16:03:57 +00:00
} ;
2019-11-29 04:32:51 +00:00
numApplicationsLoaded + + ;
OnApplicationAdded ( new ApplicationAddedEventArgs ( )
{
AppData = data ,
NumAppsFound = numApplicationsFound ,
NumAppsLoaded = numApplicationsLoaded
} ) ;
2019-09-02 16:03:57 +00:00
}
}
2019-11-29 04:32:51 +00:00
protected static void OnApplicationAdded ( ApplicationAddedEventArgs e )
{
ApplicationAdded ? . Invoke ( null , e ) ;
}
2019-09-02 16:03:57 +00:00
private static byte [ ] GetResourceBytes ( string resourceName )
{
Stream resourceStream = Assembly . GetCallingAssembly ( ) . GetManifestResourceStream ( resourceName ) ;
byte [ ] resourceByteArray = new byte [ resourceStream . Length ] ;
resourceStream . Read ( resourceByteArray ) ;
return resourceByteArray ;
}
2019-11-29 04:32:51 +00:00
private static IFileSystem GetControlFs ( PartitionFileSystem pfs )
2019-09-02 16:03:57 +00:00
{
Nca controlNca = null ;
2019-11-29 04:32:51 +00:00
// Add keys to key set if needed
foreach ( DirectoryEntryEx ticketEntry in pfs . EnumerateEntries ( "/" , "*.tik" ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
Result result = pfs . OpenFile ( out IFile ticketFile , ticketEntry . FullPath , OpenMode . Read ) ;
2019-09-02 16:03:57 +00:00
2019-10-17 06:17:44 +00:00
if ( result . IsSuccess ( ) )
2019-09-02 16:03:57 +00:00
{
2019-10-17 06:17:44 +00:00
Ticket ticket = new Ticket ( ticketFile . AsStream ( ) ) ;
2019-11-29 04:32:51 +00:00
_keySet . ExternalKeySet . Add ( new RightsId ( ticket . RightsId ) , new AccessKey ( ticket . GetTitleKey ( _keySet ) ) ) ;
2019-09-02 16:03:57 +00:00
}
}
// Find the Control NCA and store it in variable called controlNca
2019-11-29 04:32:51 +00:00
foreach ( DirectoryEntryEx fileEntry in pfs . EnumerateEntries ( "/" , "*.nca" ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
pfs . OpenFile ( out IFile ncaFile , fileEntry . FullPath , OpenMode . Read ) . ThrowIfFailure ( ) ;
2019-10-17 06:17:44 +00:00
2019-11-29 04:32:51 +00:00
Nca nca = new Nca ( _keySet , ncaFile . AsStorage ( ) ) ;
2019-10-17 06:17:44 +00:00
if ( nca . Header . ContentType = = NcaContentType . Control )
2019-09-02 16:03:57 +00:00
{
controlNca = nca ;
}
}
// Return the ControlFS
2019-11-29 04:32:51 +00:00
return controlNca ? . OpenFileSystem ( NcaSectionType . Data , IntegrityCheckLevel . None ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private static ( bool favorite , string timePlayed , string lastPlayed ) GetMetadata ( string titleId )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
string metadataFolder = Path . Combine ( new VirtualFileSystem ( ) . GetBasePath ( ) , "games" , titleId , "gui" ) ;
string metadataFile = Path . Combine ( metadataFolder , "metadata.json" ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
IJsonFormatterResolver resolver = CompositeResolver . Create ( StandardResolver . AllowPrivateSnakeCase ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
if ( ! File . Exists ( metadataFile ) )
{
Directory . CreateDirectory ( metadataFolder ) ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
_appMetadata = new ApplicationMetadata
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
Favorite = false ,
TimePlayed = 0 ,
LastPlayed = "Never"
} ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
byte [ ] saveData = JsonSerializer . Serialize ( _appMetadata , resolver ) ;
File . WriteAllText ( metadataFile , Encoding . UTF8 . GetString ( saveData , 0 , saveData . Length ) . PrettyPrintJson ( ) ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
using ( Stream stream = File . OpenRead ( metadataFile ) )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
_appMetadata = JsonSerializer . Deserialize < ApplicationMetadata > ( stream , resolver ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
return ( _appMetadata . Favorite , ConvertSecondsToReadableString ( _appMetadata . TimePlayed ) , _appMetadata . LastPlayed ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private static string ConvertSecondsToReadableString ( double seconds )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
const int secondsPerMinute = 60 ;
const int secondsPerHour = secondsPerMinute * 60 ;
const int secondsPerDay = secondsPerHour * 24 ;
string readableString ;
if ( seconds < secondsPerMinute )
{
readableString = $"{seconds}s" ;
}
else if ( seconds < secondsPerHour )
{
readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins" ;
}
else if ( seconds < secondsPerDay )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs" ;
2019-09-02 16:03:57 +00:00
}
else
{
2019-11-29 04:32:51 +00:00
readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days" ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
return readableString ;
2019-09-02 16:03:57 +00:00
}
}
}