Save Instance Prints To File

This commit is contained in:
Natsumi
2024-11-17 10:11:52 +13:00
parent 7956e19a10
commit 78a6dfbfe1
10 changed files with 223 additions and 82 deletions

View File

@@ -30,7 +30,6 @@ namespace VRCX
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly MD5 _hasher = MD5.Create();
private static bool dialogOpen;
static AppApi()
{
@@ -575,5 +574,16 @@ namespace VRCX
return null;
}
public async Task<bool> SavePrintToFile(string url, string fileName)
{
var directory = Path.Combine(GetVRChatPhotosLocation(), "Prints");
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, fileName);
if (File.Exists(path))
return false;
return await ImageCache.SaveImageToFile(url, path);
}
}
}

View File

@@ -12,6 +12,8 @@ namespace VRCX
{
public partial class AppApi
{
private static bool dialogOpen;
/// <summary>
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
/// </summary>

View File

@@ -6,89 +6,119 @@ using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace VRCX
namespace VRCX;
internal static class ImageCache
{
internal static class ImageCache
private static readonly string cacheLocation;
private static readonly HttpClient httpClient;
private static readonly List<string> _imageHosts =
[
"api.vrchat.cloud",
"files.vrchat.cloud",
"d348imysud55la.cloudfront.net",
"assets.vrchat.com"
];
static ImageCache()
{
private static readonly string cacheLocation;
private static readonly HttpClient httpClient;
private static readonly List<string> _imageHosts =
[
"api.vrchat.cloud",
"files.vrchat.cloud",
"d348imysud55la.cloudfront.net",
"assets.vrchat.com"
];
cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
var httpClientHandler = new HttpClientHandler();
if (WebApi.ProxySet)
httpClientHandler.Proxy = WebApi.Proxy;
httpClient = new HttpClient(httpClientHandler);
}
static ImageCache()
public static async Task<string> GetImage(string url, string fileId, string version)
{
var directoryLocation = Path.Combine(cacheLocation, fileId);
var fileLocation = Path.Combine(directoryLocation, $"{version}.png");
if (File.Exists(fileLocation))
{
cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
var httpClientHandler = new HttpClientHandler();
if (WebApi.ProxySet)
httpClientHandler.Proxy = WebApi.Proxy;
httpClient = new HttpClient(httpClientHandler);
}
public static async Task<string> GetImage(string url, string fileId, string version)
{
var directoryLocation = Path.Combine(cacheLocation, fileId);
var fileLocation = Path.Combine(directoryLocation, $"{version}.png");
if (File.Exists(fileLocation))
{
Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
return fileLocation;
}
if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream);
}
}
var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
return fileLocation;
}
private static void CleanImageCache()
if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
var dirInfo = new DirectoryInfo(cacheLocation);
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000);
foreach (var folder in folders)
CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
folder.Delete(true);
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using (var response = await httpClient.SendAsync(request))
{
response.EnsureSuccessStatusCode();
await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fileStream);
}
}
var cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
return fileLocation;
}
private static void CleanImageCache()
{
var dirInfo = new DirectoryInfo(cacheLocation);
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000);
foreach (var folder in folders)
{
folder.Delete(true);
}
}
public static async Task<bool> SaveImageToFile(string url, string path)
{
var uri = new Uri(url);
if (!_imageHosts.Contains(uri.Host))
throw new ArgumentException("Invalid image host", url);
var cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
var cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri("https://api.vrchat.cloud"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers =
{
{ "Cookie", cookieString },
{ "User-Agent", Program.Version }
}
};
using var response = await httpClient.SendAsync(request);
if (!response.IsSuccessStatusCode)
return false;
await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
return true;
}
}

View File

@@ -5447,7 +5447,11 @@ speechSynthesis.getVoices();
}
}
this.lastLocation.playerList.forEach((ref1) => {
if (ref1.userId && !API.cachedUsers.has(ref1.userId)) {
if (
ref1.userId &&
typeof ref1.userId === 'string' &&
!API.cachedUsers.has(ref1.userId)
) {
API.getUser({ userId: ref1.userId });
}
});
@@ -8149,6 +8153,10 @@ speechSynthesis.getVoices();
'VRCX_StartAtWindowsStartup',
this.isStartAtWindowsStartup
);
await configRepository.setBool(
'VRCX_saveInstancePrints',
this.saveInstancePrints
);
VRCXStorage.Set(
'VRCX_StartAsMinimizedState',
this.isStartAsMinimizedState.toString()
@@ -17522,6 +17530,52 @@ speechSynthesis.getVoices();
}
});
API.getPrint = function (params) {
return this.call(`prints/${params.printId}`, {
method: 'GET'
}).then((json) => {
var args = {
json,
params
};
this.$emit('PRINT', args);
return args;
});
};
API.editPrint = function (params) {
return this.call(`prints/${params.printId}`, {
method: 'POST',
params
}).then((json) => {
var args = {
json,
params
};
this.$emit('PRINT:EDIT', args);
return args;
});
};
$app.data.saveInstancePrints = await configRepository.getBool(
'VRCX_saveInstancePrints',
false
);
$app.methods.trySavePrintToFile = async function (printId) {
var print = await API.getPrint({ printId });
var imageUrl = print.json?.files?.image;
if (!imageUrl) {
console.error('Print image URL is missing', print);
return;
}
var fileName = `${printId}.png`;
var status = await AppApi.SavePrintToFile(imageUrl, fileName);
if (status) {
console.log(`Print saved to file: ${fileName}`);
}
};
// #endregion
// #region | Emoji

View File

@@ -39,7 +39,7 @@ export default class extends baseClass {
if (this.gameLogDisabled) {
return;
}
var userId = gameLog.userId;
var userId = String(gameLog.userId || '');
if (!userId && gameLog.displayName) {
for (var ref of API.cachedUsers.values()) {
if (ref.displayName === gameLog.displayName) {
@@ -257,6 +257,25 @@ export default class extends baseClass {
// if (!userId) {
// break;
// }
if (!$app.saveInstancePrints) {
break;
}
try {
var printId = '';
var url = new URL(gameLog.url);
if (
url.pathname.substring(0, 14) === '/api/1/prints/'
) {
var pathArray = url.pathname.split('/');
printId = pathArray[4];
}
if (printId && printId.length === 41) {
$app.trySavePrintToFile(printId);
}
} catch (err) {
console.error(err);
}
break;
case 'avatar-change':
var ref = this.lastLocation.playerList.get(userId);

View File

@@ -598,6 +598,9 @@ export default class extends baseClass {
},
key() {
this.parse();
},
userid() {
this.parse();
}
},
mounted() {

View File

@@ -514,19 +514,34 @@ export default class extends baseClass {
var contentType = content.contentType;
console.log('content-refresh', content);
if (contentType === 'icon') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogIconsLoading
) {
$app.refreshVRCPlusIconsTable();
}
} else if (contentType === 'gallery') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogGalleryLoading
) {
$app.refreshGalleryTable();
}
} else if (contentType === 'emoji') {
if ($app.galleryDialogVisible) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogEmojisLoading
) {
$app.refreshEmojiTable();
}
} else if (contentType === 'print') {
if ($app.galleryDialogVisible) {
} else if (
contentType === 'print' ||
contentType === 'prints'
) {
if (
$app.galleryDialogVisible &&
!$app.galleryDialogPrintsLoading
) {
$app.refreshPrintTable();
}
} else if (contentType === 'avatar') {

View File

@@ -433,6 +433,10 @@
"header": "Local World Persistence",
"description": "Localhost webserver (requires restart)"
},
"save_instance_prints_to_file": {
"header": "Save Instance Prints To File",
"description": "Save spawned prints to your VRChat Pictures folder"
},
"remote_database": {
"header": "Remote Avatar Database",
"enable": "Enable",

View File

@@ -32,7 +32,7 @@ mixin screenshotMetadata()
br
location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name")
br
display-name(v-if="screenshotMetadataDialog.metadata.author" :userid="screenshotMetadataDialog.metadata.author.id" :hind="screenshotMetadataDialog.metadata.author.displayName" style="color:#909399;font-family:monospace")
display-name(v-if="screenshotMetadataDialog.metadata.author" :userid="screenshotMetadataDialog.metadata.author.id" :hint="screenshotMetadataDialog.metadata.author.displayName" style="color:#909399;font-family:monospace")
br
el-carousel(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange")
el-carousel-item

View File

@@ -475,6 +475,10 @@ mixin settingsTab()
div.options-container-item
span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.local_world_persistence.description') }}
el-switch(v-model="disableWorldDatabase" :active-value="false" :inactive-value="true" @change="saveVRCXWindowOption")
span.sub-header {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.header') }}
div.options-container-item
span.name(style="min-width:300px") {{ $t('view.settings.advanced.advanced.save_instance_prints_to_file.description') }}
el-switch(v-model="saveInstancePrints" @change="saveVRCXWindowOption")
//- Advanced | Remote Avatar Database
div.options-container
span.header {{ $t('view.settings.advanced.advanced.remote_database.header') }}