Skip to content

Instantly share code, notes, and snippets.

@yanonono
Last active February 22, 2026 08:38
Show Gist options
  • Select an option

  • Save yanonono/b55701c8e80517e5d0cebbd3e5023b33 to your computer and use it in GitHub Desktop.

Select an option

Save yanonono/b55701c8e80517e5d0cebbd3e5023b33 to your computer and use it in GitHub Desktop.
VRChatで撮影した写真をワールドごとのフォルダに選別します

VRChat Photo Selector

このファイルについて

 VRCHatのVRモードでカメラ撮影した画像ファイルをワールドごとのフォルダに仕分けします。

実行環境

  • Windows 10, Version 2004 以降
    • Verison 1803などでも動くと思いますが、Microsoftのライフサイクルから外れているため動作対象外になります。
  • PowerShell 5.1

実行方法(Windows GUI)

  1. 下記のスクリプトをメモ帳などに貼り付けます
  2. ファイル名を"VRC_PhotoSelector.ps1"で保存します
  3. 保存した"VRC_PhotoSelector.ps1"を右クリックします
  4. 各種パラメータを設定して、右下の"Run"をクリックします
  5. 指定されたピクチャフォルダの画像ファイルが「日時+ワールド名」のフォルダに移動します
  6. 終了するには、下の"Exit"をクリックします

ライセンス

MIT License

【更新履歴】

  • v1.11 2022/04/09
    • 年月フォルダ以外にファイルが存在しないときにエラーが発生する不具合の修正。
    • readme.txtの文字コードをUTF-8(BOM付き)に変更。
  • v1.10b 2021/10/13
    • 入力待ちが発生してしまう不具合修正。
  • v1.10 2021/10/09
    • VRChatの画像ファイル保存場所の変更に伴う修正。
  • v1.09b 2021/03/11
    • World IDが正しく取得できない不具合の修正。
  • v1.09 2021/03/11
    • ユーザーへJOINが発生したときに、正しく振り分けされない不具合の修正。
  • v1.08 2021/02/05
    • VRChatのログ表記方式の変更(RoomManagerの廃止)に対応。
  • v1.07 2020/05/06
    • 設定をファイル(setting.json)に記録する形式に変更。
    • オプションとしてハッシュタグなどを登録できるように追加。
    • 画像の数だけテキストファイルを作成していた不具合の修正。
  • v1.06 2020/04/30
    • ログに記載されている時刻が00:00~09:59(午前中の桁数が1桁時間)のとき、時刻の前にゼロが追加されていない不具合修正。
  • v1.05 2019/10/14
    • ワールド情報のテキストファイルにURLやVRChatワールド紹介用ハッシュタグを出力する機能追加
  • v1.04 2019/02/22
    • ログファイルを選択するコンボボックスに、ログファイル名+最終更新日が表示されるように修正。
  • v1.03 2019/02/22
    • ワールド情報のテキストファイルを出力する機能追加。
  • v1.02 2019/02/21
    • GUI版実行時、移動するファイルごとにポップアップが表示されないように修正。
  • v1.01 2019/02/21
    • ワールド名に含まれる記号を全角変換する文字を追加。readme.mdを追加。
  • v1.00 2019/02/20
    • 初版公開
$originalDebugPreference = $DebugPreference
$DebugPreference = "SilentlyContinue" # Continue SilentlyContinue
### read setting.json file
set-variable -name SETTING_JSON_FILE -value ".\setting.json" -option constant
if (Test-Path $SETTING_JSON_FILE) {
$settingJson = ConvertFrom-Json -InputObject (Get-Content $SETTING_JSON_FILE -Encoding UTF8 -Raw)
}
else {
$settingJson = [PSCustomObject] @{
PhotoPath = $env:USERPROFILE + "\Pictures\VRChat";
logPath = $env:LOCALAPPDATA + "Low\VRChat\vrchat";
MakeWorldInfoFile = $TRUE;
WithTweetText = $TRUE;
IncludeTweetWldLink = $TRUE;
IncludeTweetText = "#VRChat_world紹介 #MadeWithVRChat";
}
}
### ser GUI Form
[void][reflection.assembly]::LoadWithPartialName("Microsoft.VisualBasic")
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.Windows.Forms
[xml]$xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="VRCHat Photo Selector" Height="330" Width="640" MinWidth="640" MinHeight="330">
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="50"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75*"/>
<ColumnDefinition Width="150*"/>
<ColumnDefinition Width="75*"/>
</Grid.ColumnDefinitions>
<Label x:Name="lblPhotoPath" Content="Photos filepath" Margin="10,15,18,0" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Height="26" />
<TextBox x:Name="txtPhotoPath" Text="" Height="25" VerticalAlignment="Top" Grid.Row="0" Grid.Column="1" Margin="10,15,16,0" />
<Button x:Name="btnSelectPhotoPath" Content="Select" Margin="10,15,10,0" VerticalAlignment="Top" Height="25" Grid.Row="0" Grid.Column="2" />
<Label x:Name="lblFilePath" Content="Log filepath" Margin="10,15,18,0" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" Height="26" />
<TextBox x:Name="txtLogPath" Text="" Height="25" VerticalAlignment="Top" Grid.Row="1" Grid.Column="1" Margin="10,15,16,0" />
<Button x:Name="btnSelectLogPath" Content="Select" Margin="10,15,10,0" VerticalAlignment="Top" Height="25" Grid.Row="1" Grid.Column="2" />
<Label x:Name="lblLogFile" Content="Logfile" Margin="10,16,18,0" Grid.Row="2" Grid.Column="0" VerticalAlignment="Top" Height="24"/>
<ComboBox x:Name="cmbLogFile" Height="25" VerticalAlignment="Top" Margin="10,15,16,0" Grid.Row="2" Grid.Column="1" />
<Button x:Name="btnRun" Content="Run" Margin="10,15,10,10" Grid.Row="2" Grid.Column="2" Grid.RowSpan="2"/>
<CheckBox x:Name="cbMakeWorldInfoFile" Content="Make world information textfile." Grid.Column="1" HorizontalAlignment="Left" Margin="10,15,0,0" Grid.Row="3" VerticalAlignment="Top" IsChecked="True"/>
<Button x:Name="btnExit" Content="Exit" Margin="10,10,10,10" VerticalAlignment="Bottom" Height="37" Grid.Row="3" Grid.Column="0" />
<CheckBox x:Name="cbWithTweetText" Content="Make world tweet text." Grid.Column="1" HorizontalAlignment="Left" Margin="10,35,0,0" Grid.Row="3" VerticalAlignment="Top" IsChecked="True"/>
<CheckBox x:Name="cbIncludeTweetWldLink" Content="With VRChat world-url/link." Grid.Column="1" HorizontalAlignment="Left" Margin="30,60,0,0" Grid.Row="3" VerticalAlignment="Top" />
<Label x:Name="lblIncludeTweetText" Content="Tweet text/hashtags" Margin="28,75,0,0" Grid.Column="1" Grid.Row="3"/>
<TextBox x:Name="txtIncludeTweetText" Text="" Height="25" Width="270" Grid.Column="1" HorizontalAlignment="Left" Margin="30,95,10,10" Grid.Row="3" VerticalAlignment="Top" />
</Grid>
</Window>
'@
$reader = New-Object System.Xml.XmlNodeReader $xaml
$frm = [System.Windows.Markup.XamlReader]::Load($reader)
# exit button
$frmBtnExit = $frm.FindName("btnExit")
$frmBtnExit.Add_Click( {
#save setting.json
$frmPhotoPath = $frm.FindName("txtPhotoPath")
$frmLogPath = $frm.FindName("txtLogPath")
$frmMakeWorldInfoFile = $frm.FindName("cbMakeWorldInfoFile")
$frmWithTweetText = $frm.FindName("cbWithTweetText")
$frmIncludeTweetWldLink = $frm.FindName("cbIncludeTweetWldLink")
$frmIncludeTweetText = $frm.FindName("txtIncludeTweetText")
$settingJson = @{
PhotoPath = $frmPhotoPath.text;
logPath = $frmLogPath.text
MakeWorldInfoFile = $frmMakeWorldInfoFile.IsChecked;
WithTweetText = $frmWithTweetText.IsChecked;
IncludeTweetWldLink = $frmIncludeTweetWldLink.IsChecked
IncludeTweetText = $frmIncludeTweetText.text;
}
$settingJson | ConvertTo-Json | Out-File $SETTING_JSON_FILE -Encoding UTF8
$frm.close()
})
# text PhotoPath
$frmPhotoPath = $frm.FindName("txtPhotoPath")
$frmPhotoPath.text = $settingJson.PhotoPath
#select-button PhotoPath
$frmBtnSelectPhotoPath = $frm.FindName("btnSelectPhotoPath")
$frmBtnSelectPhotoPath.Add_Click( {
$frmPhotoPath = $frm.FindName("txtPhotoPath")
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = "PNG File (*.png)|*.png|All Files (*.*)|*.*"
$dialog.InitialDirectory = $frmPhotoPath.text
$dialog.Title = "Select VRChat Photo Folder"
if ($dialog.ShowDialog() -eq "OK") {
$path = Split-Path -Parent $dialog.FileName
$frmPhotoPath.text = $path
}
})
# text LogPath
$logPath = $settingJson.logPath
$frmLogPath = $frm.FindName("txtLogPath")
$frmLogPath.text = $logPath
function setLogFilesName($Path) {
$frmCmbLogFile = $frm.FindName("cmbLogFile")
$frmCmbLogFile.Items.Clear();
Get-ChildItem $Path |
Sort-Object LastWriteTime -Descending |
Where-Object { $_.Extension -eq ".txt" } |
ForEach-Object {
$filename = $_.Name + "`t" + "[Updated:" + $(Get-ItemProperty $_.FullName).LastWriteTime + "]"
[void]$frmCmbLogFile.Items.Add($filename)
}
}
setLogFilesName($logPath)
#select-button LogPath
$frmBtnSelectLogPath = $frm.FindName("btnSelectLogPath")
$frmBtnSelectLogPath.Add_Click( {
$frmLogPath = $frm.FindName("txtLogPath")
$dialog = New-Object System.Windows.Forms.OpenFileDialog
$dialog.Filter = "Text File (*.txt)|*.txt|All Files (*.*)|*.*"
$dialog.InitialDirectory = $frmLogPath.text
$dialog.Title = "Select VRChat Logfile"
if ($dialog.ShowDialog() -eq "OK") {
$path = Split-Path -Parent $dialog.FileName
$frmLogPath.text = $path
# select combobox item
$frmCmbLogFile = $frm.FindName("cmbLogFile")
setLogFilesName($path)
$filename = [System.IO.Path]::GetFileName($dialog.FileName)
for ($i = 0; $i -lt $frmCmbLogFile.Items.Count; $i++) {
$tmpName = $frmCmbLogFile.Items[$i].ToString().split(" ")
if ($tmpName[0] -eq $filename) {
$frmCmbLogFile.SelectedIndex = $i
}
}
}
})
# checkbox cbMakeWorldInfoFile
$frmMakeWorldInfoFile = $frm.FindName("cbMakeWorldInfoFile")
$frmMakeWorldInfoFile.IsChecked = $settingJson.MakeWorldInfoFile
# checkbox cbWithTweetText
$frmWithTweetText = $frm.FindName("cbWithTweetText")
$frmWithTweetText.IsChecked = $settingJson.WithTweetText
# checkbox cbIncludeTweetWldLink
$frmIncludeTweetWldLink = $frm.FindName("cbIncludeTweetWldLink")
$frmIncludeTweetWldLink.IsChecked = $settingJson.IncludeTweetWldLink
# textbox txtIncludeTweetText
$frmIncludeTweetText = $frm.FindName("txtIncludeTweetText")
$frmIncludeTweetText.text = $settingJson.IncludeTweetText
# button RUN
$frmBtnRun = $frm.FindName("btnRun")
$frmBtnRun.Add_Click( {
$frmLogPath = $frm.FindName("txtLogPath")
$frmCmbLogFile = $frm.FindName("cmbLogFile")
if ($frmCmbLogFile.SelectedIndex -lt 0) {
[System.Windows.Forms.MessageBox]::Show("VRCHat logfile is not select.", "Error")
return
}
$tmpName = $frmCmbLogFile.Items[$frmCmbLogFile.SelectedIndex].ToString().split("`t")
$logfile = $frmLogPath.text + "\" + $tmpName[0]
if (Test-Path $logfile) {
}
else {
[System.Windows.Forms.MessageBox]::Show("VRCHat logfile is not found.", "Error")
return
}
$frmPhotoPath = $frm.FindName("txtPhotoPath")
$photodir = $frmPhotoPath.text
if (Test-Path $photodir) {
}
else {
[System.Windows.Forms.MessageBox]::Show("VRCHat Photos Folder is not found.", "Error")
return
}
$enc = [System.Text.Encoding]::UTF8
$parser = New-Object -TypeName Microsoft.VisualBasic.FileIO.TextFieldParser $logfile, $enc
$parser.TextFieldType = [Microsoft.VisualBasic.FileIO.FieldType]::FixedWidth
$parser.SetFieldWidths(20, 11, 3, -1)
## Parse VRChat Logfile.
$target = ""
$dtEnd = ""
$isInRoom = $FALSE
$listWorlds = @{ }
While ($parser.EndOfData -eq $false) {
try {
$fields = $parser.ReadFields()
# skip brankline
if ($fields -eq "") {
continue
}
$dateTime = [System.Datetime]::Parse($fields[0].Trim()) # Date Time
$loglevel = $fields[1].Trim() # log level
$dummy = $fields[2] #
$text = $fields[3].Trim() # text
Write-Debug ("text(fields[3]): " + $text)
}
catch {
continue
}
if ($text.StartsWith("[")) {
$target = $text.Substring(1, $text.IndexOf("]") - 1)
Write-Debug ("(target)" + $target)
}
# check into room
if ($text.Contains("Entering world")) {
$isInRoom = $TRUE
}
Write-Debug ("isInRoom: " + $isInRoom)
# RoomManager
if ("RoomManager" -eq $target -Or $isInRoom -eq $TRUE) {
# Join room
$keyword = "Entering Room:"
if ($text.Contains($keyword)) {
$dtBegin = $dateTime
Write-Debug ("EnteringRoom(text): " + $text)
$posFrom = $text.IndexOf($keyword) + $keyword.Length
$length = $text.Length - $posFrom
$worldTitle = $text.Substring($posFrom, $length).Trim()
Write-Debug ("EnteringRoom(dateTime):" + $dateTime)
Write-Debug ("EnteringRoom(target):" + $target)
Write-Debug ("EnteringRoom(worldTitle):" + $worldTitle)
}
# World ID
$keyword = "Joining"
if ($text.Contains("Joining wrld_") ) {
$dtBegin = $dateTime
$posFrom = $text.IndexOf($keyword) + $keyword.Length
$posEnd = $text.IndexOf(":")
$length = $posEnd - $posFrom
$worldTempId = $text.Substring($posFrom, $length).Trim()
if ($worldTempId.Substring(0, 4) -eq "wrld") {
$worldId = $worldTempId
Write-Debug ("Joining(worldID): " + $worldId)
}
}
# leave room
$keyword = "Successfully left room"
if ($text.Contains($keyword)) {
$dtEnd = $dateTime
Write-Debug ("Action log: " + $dtBegin + ", " + $dateTime + ", " + $worldTitle + "," + $worldId )
$listWorlds.Add($listWorlds.Count, @($dtBegin, $dtEnd, $worldTitle, $worldId))
$dtEnd = ""
$isInRoom = $FALSE
}
}
}
if ($dtEnd -eq "") {
Write-Debug ("Action log: " + $dtBegin + ", " + $dateTime + ", " + $worldTitle + "," + $worldId )
$listWorlds.Add($listWorlds.Count, @($dtBegin, $dateTime, $worldTitle, $worldId))
}
$listWorlds
$frmCbMakeWorldInfoFile = $frm.FindName("cbMakeWorldInfoFile")
$isMakeWorldFile = $frmCbMakeWorldInfoFile.IsChecked
$frmCbWithTweetText = $frm.FindName("cbWithTweetText")
$isWithTweetText = $frmCbWithTweetText.IsChecked
$frmIncludeTweetWldLink = $frm.FindName("cbIncludeTweetWldLink")
$isIncludeTweetWldLink = $frmIncludeTweetWldLink.IsChecked
$frmIncludeTweetText = $frm.FindName("txtIncludeTweetText")
$txtIncludeTweetText = $frmIncludeTweetText.text
Write-Debug ("isMakeWorldFile = " + $isMakeWorldFile)
Write-Debug ("isWithTweetText = " + $isWithTweetText)
Write-Debug ("isIncludeTweetWldLink = " + $isIncludeTweetWldLink)
Write-Debug ("txtIncludeTweetText = " + $txtIncludeTweetText)
Write-Debug ("listWorlds.Count = " + $listWorlds.Count)
# pickup photo files
$photoFiles = @()
$photoFiles += Get-ChildItem $photodir | Where-Object { ! $_.PSIsContainer } # photo directory(-2021-09)
$photoFiles += Get-ChildItem -Recurse $photodir | Where-Object { (! $_.PSIsContainer) -And ($_.Directory.Name -match "20[0-9]{2}-[0|1][0-9]") } # photo directory(2021-10, yyyy-mm)
for ($i = 0; $i -lt $listWorlds.Count; $i++) {
Write-Debug([string]::Concat($i, ",", $listWorlds[$i][0], "", $listWorlds[$i][1], ", ", $listWorlds[$i][3], ", ", $listWorlds[$i][2]))
$isSetTextfile = $FALSE
$photoFiles |
Where-Object { ($_.LastWriteTime -ge $listWorlds[$i][0]) -and ($_.LastWriteTime -le $listWorlds[$i][1]) } |
ForEach-Object {
Write-Debug ("Target photo file: " + $_.Name + ", " + $_.LastWriteTime)
if ( $_.Directory.Name -match "20[0-9]{2}-[0|1][0-9]" ) {
$dirname = $_.Directory.Name + "\" + $listWorlds[$i][0].ToString("yyyyMMdd-HHmmss") + " " + $listWorlds[$i][2].Replace('>', '').Replace('<', '').Replace('\', '').Replace('|', '')
}
else {
$dirname = $listWorlds[$i][0].ToString("yyyyMMdd-HHmmss") + " " + $listWorlds[$i][2].Replace('>', '').Replace('<', '').Replace('\', '').Replace('|', '')
}
if (Test-Path ($photodir + "\" + $dirname)) {
}
else {
New-Item ($photodir + "\" + $dirname) -ItemType Directory
}
if ($isSetTextfile -eq $FALSE) {
$textValue = ""
if ($isMakeWorldFile -eq $TRUE) {
$textValue = "Joined at " + $listWorlds[$i][0] + "`r`n",
"Leaved at " + $listWorlds[$i][1] + "`r`n",
"`r`n",
"World name : " + $listWorlds[$i][2] + "`r`n",
"World ID : " + $listWorlds[$i][3] + "`r`n"
$textValue = $textValue + "`r`n"
}
if ($isWithTweetText -eq $TRUE) {
if ($textValue -ne "") {
$textValue = $textValue + "`r`n" + "-----------------" + "`r`n"
}
$textValue = $textValue + $listWorlds[$i][2] + "`r`n"
if ($txtIncludeTweetText -ne "") {
$textValue = $textValue + $txtIncludeTweetText + "`r`n"
}
if ($isIncludeTweetWldLink -eq $TRUE) {
$textValue = $textValue + "https://vrchat.com/home/launch?worldId=" + $listWorlds[$i][3] + "`r`n"
}
}
if ($textValue -ne "") {
$worldFileName = $photodir + "\" + $dirname + "\World_" + $listWorlds[$i][0].ToString("yyyyMMdd-HHmmss")
$textValue = $textValue -replace "`r`n ", "`r`n"
$worldTextName = $worldFileName + ".txt"
Write-Debug ("world text file: " + $worldTextName)
Set-Content -Path $worldTextName -Encoding UTF8 -Value $textValue
# $listWorlds[$i] | ConvertTo-Json | Out-File -LiteralPath ($worldFileName + ".json") -Encoding utf8
}
}
$isSetTextfile = $TRUE
if (Test-Path ($_.FullName)) {
Write-Debug ("Move file: " + ($_.FullName) + "" + ($photodir + "\" + $dirname))
Move-Item ($_.FullName) ($photodir + "\" + $dirname)
}
else {
Write-Debug ("Error photo: " + $_.FullName)
}
}
}
[System.Windows.Forms.MessageBox]::Show("Selected photos", "Finish")
})
$result = $frm.ShowDialog()
$DebugPreference = $originalDebugPreference
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment