-
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathobs-powershell.build.ps1
354 lines (300 loc) · 14.1 KB
/
obs-powershell.build.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#requires -Module PipeScript
# The WebSocket is nice enough to provide it's documentation in JSON
$obsWebSocketProtocol = Invoke-RestMethod https://raw.githubusercontent.com/obsproject/obs-websocket/master/docs/generated/protocol.json
# This will save us lots of time and effort for parsing
# We'll want to translate OBS WebSocket requests into commands.
# We'll want a prefix on all commands.
$modulePrefix = 'OBS'
# And we'll want a hashtable of replacements
# A number of request types start with a verb name already
$verbReplacements = @{}
# these are easy
foreach ($easyVerb in 'Get', 'Set','Open', 'Close', 'Start', 'Stop', 'Resume','Remove','Save','Send') {
$verbReplacements[$easyVerb] = $easyVerb
}
# A number of other words cases should also infer the verb
$verbReplacements += [Ordered]@{
'Toggle' = 'Switch' # Toggle infers switch
'Create' = 'Add' # Create infers add
'Duplicate' = 'Copy' # Duplicate infers copy
'Broadcast' = 'Send' # Broadcast infers Send
'List' = 'Get' # List infers get (but may cause duplication problems)
}
# Construct a pair of regex to see if something starts or ends with our replacements
$startsWithVerbRegex = "^(?>$($verbReplacements.Keys -join '|'))"
$endsWithVerbRegex = "(?>$($verbReplacements.Keys -join '|'))$"
# We also want a pair of regexes to determine if a value has a min/max range.
$minRangeRestriction = "\>=\s{0,}(?<min>[\d\.-]+)"
$maxRangeRestriction = "\<=\s{0,}(?<max>[\d\.-]+)"
# Create an array to hold all of the functions we create
$obsFunctions = @()
# and files we build.
$filesBuilt = @()
# And determine where we want to store them
$commandsPath = Join-Path $PSScriptRoot Commands
$requestsPath = Join-Path $commandsPath Requests
# (create the directory if it didn't already exist)
if (-not (Test-Path $requestsPath)) {
$null = New-Item -ItemType Directory -Path $requestsPath -Force
}
$ToAlias = @{
"Add-OBSSceneItem" = "Add-OBSSceneSource"
}
$PostProcess = @{
"Save-OBSSourceScreenshot" = {
Get-Item $paramCopy["imageFilePath"] |
Add-Member NoteProperty InputName $paramCopy["SourceName"] -Force -PassThru |
Add-Member NoteProperty SourceName $paramCopy["SourceName"] -Force -PassThru |
Add-Member NoteProperty ImageWidth $paramCopy["ImageWidth"] -Force -PassThru |
Add-Member NoteProperty ImageHeight $paramCopy["ImageHeight"] -Force -PassThru
}
}
# Declare the process block for all commands now
$obsFunctionProcessBlock = {
# Create a copy of the parameters (that are part of the payload)
$paramCopy = [Ordered]@{}
# get a reference to this command
$myCmd = $MyInvocation.MyCommand
# Keep track of how many requests we have done of a given type
# (this makes creating RequestIDs easy)
if (-not $script:ObsRequestsCounts) {
$script:ObsRequestsCounts = @{}
}
# Set my requestType to blank
$myRequestType = ''
# and indicate we are not expecting a response
$responseExpected = $false
# Then walk over this commands' attributes,
foreach ($attr in $myCmd.ScriptBlock.Attributes) {
if ($attr -is [Reflection.AssemblyMetadataAttribute]) {
if ($attr.Key -eq 'OBS.WebSocket.RequestType') {
$myRequestType = $attr.Value # set the requestType,
}
elseif ($attr.Key -eq 'OBS.WebSocket.ExpectingResponse') {
# and determine if we are expecting a response.
$responseExpected =
if ($attr.Value -eq 'false') {
$false
} else { $true }
}
}
}
# Walk over each parameter
:nextParam foreach ($keyValue in $PSBoundParameters.GetEnumerator()) {
# and walk over each of it's attributes to see if it part of the payload
foreach ($attr in $myCmd.Parameters[$keyValue.Key].Attributes) {
# If the parameter is bound to part of the payload
if ($attr -is [ComponentModel.DefaultBindingPropertyAttribute]) {
# copy it into our payload dicitionary.
$paramCopy[$attr.Name] = $keyValue.Value
# (don't forget to turn switches into booleans)
if ($paramCopy[$attr.Name] -is [switch]) {
$paramCopy[$attr.Name] = [bool]$paramCopy[$attr.Name]
}
if ($attr.Name -like '*path') {
$paramCopy[$attr.Name] =
"$($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($paramCopy[$attr.Name]))"
}
continue nextParam
}
}
}
# and make a request ID from that.
$myRequestId = "$myRequestType.$([Guid]::newGuid())"
# Construct the payload object
$requestPayload = [Ordered]@{
# It must include a request ID
requestId = $myRequestId
# request type
requestType = $myRequestType
# and optional data
requestData = $paramCopy
}
if ($PassThru) {
[PSCustomObject]$requestPayload
} else {
[PSCustomObject]$requestPayload |
Send-OBS -NoResponse:$NoResponse
}
}
# Walk over each type of request.
foreach ($obsRequestInfo in $obsWebSocketProtocol.requests) {
$requestType = $obsRequestInfo.RequestType
$replacedRequestType = "$requestType"
# Determine the function name
$obsFunctionName =
@(
# If it started with something that inferred the verb
if ($requestType -match $startsWithVerbRegex) {
# replace the name
$replacedRequestType = ($replacedRequestType -replace $startsWithVerbRegex)
$verbReplacements[$matches.0] + '-' + $modulePrefix + $replacedRequestType
}
# If it ended with something that inferred the verb
if ($requestType -cmatch $endsWithVerbRegex) {
# replace the name again
$replacedRequestType = ($replacedRequestType -creplace $endsWithVerbRegex)
$verbReplacements[$matches.0] + '-' + $modulePrefix + $replacedRequestType
}
# If it didn't start or end with something that inferred a verb
if ($requestType -notmatch $startsWithVerbRegex -and
$requestType -notmatch $endsWithVerbRegex) {
# Use Send-
"Send-${modulePrefix}${RequestType}"
})[-1] # Pick the last output for from this set of options, as this will be the most changed.
$obsFunctionParameters = [Ordered]@{}
# Now we have to turn each field in the request into a parameter
foreach ($requestField in $obsRequestInfo.requestFields) {
$valueType = $requestField.valueType
# Some field names contain periods, don't forget to get rid of those.
$paramName = $requestField.valueName -replace '\.'
# PowerShell parameters should start with uppercase letters, so fix that.
$paramName = $paramName.Substring(0,1).ToUpper() + $paramName.Substring(1)
$paramType = # map their parameter types to PowerShell parameter types
if ($valueType -eq 'Boolean') { '[switch]'}
elseif ($valueType -eq 'Number') { '[double]'}
elseif ($valueType -eq 'Object') { '[PSObject]'}
elseif ($valueType -eq 'Any') { '[PSObject]'}
elseif ($valueType -eq 'String') { '[string]'}
else { '' }
# And declare a parameter
$obsFunctionParameters[$paramName] =
@(
# Include the description
"<# $($requestField.ValueDescription) #>"
# Make sure to declare it as ValueFromPipelineByPropertyName
"[Parameter($(
# and mark it as mandtory if it's not optional.
if (-not $requestField.ValueOptional) { "Mandatory,"}
)ValueFromPipelineByPropertyName)]"
# Track the 'bound' property
"[ComponentModel.DefaultBindingProperty('$($requestField.valueName)')]"
# If there were range descriptions
if ($requestField.valueRestrictions) {
# determine the min/max
$rangeMin, $rangeMax = $null, $null
if ($requestField.valueRestrictions -match $minRangeRestriction) {
$rangeMin = [double]$matches.min
}
if ($requestField.valueRestrictions -match $maxRangeRestriction) {
$rangeMax = [double]$matches.max
}
# and write a [ValidateRange()]
if ($rangeMin -ne $null -or $rangeMax -ne $null) {
"[ValidateRange($($rangeMin),$(if ($rangeMax) {
$rangeMax
} elseif ([Math]::Round($rangeMin) -eq $rangeMin) {
"[int]::MaxValue"
} else {
"[double]::MaxValue"
}))]"
}
}
# Include the parameter type
$paramType
# and declare the parameter.
"`$$paramName"
)
}
$obsFunctionParameters['PassThru'] = @(
"# If set, will return the information that would otherwise be sent to OBS."
"[Parameter(ValueFromPipelineByPropertyName)]"
"[Alias('OutputRequest','OutputInput')]"
"[switch]"
'$PassThru'
)
$obsFunctionParameters['NoResponse'] = @(
"# If set, will not attempt to receive a response from OBS."
"# This can increase performance, and also silently ignore critical errors"
"[Parameter(ValueFromPipelineByPropertyName)]"
"[Alias('NoReceive','IgnoreResponse','IgnoreReceive','DoNotReceiveResponse')]"
"[switch]"
'$NoResponse'
)
$newFunctionAttributes = @(
"[Reflection.AssemblyMetadata('OBS.WebSocket.RequestType', '$requestType')]"
"[Alias('obs.powershell.websocket.$RequestType')]"
if ($obsRequestInfo.responseFields.Count) {
"[Reflection.AssemblyMetadata('OBS.WebSocket.ExpectingResponse', `$true)]"
}
if ($ToAlias[$obsFunctionName]) {
"[Alias('$($ToAlias[$obsFunctionName] -join "','")')]"
}
)
$processBlock = if ($PostProcess[$obsFunctionName]) {
[scriptblock]::Create(
'' + $obsFunctionProcessBlock + [Environment]::Newline + $PostProcess[$obsFunctionName]
)
} else {
$obsFunctionProcessBlock
}
$newFunc =
New-PipeScript -FunctionName $obsFunctionName -Parameter $obsFunctionParameters -Process $processBlock -Attribute $newFunctionAttributes -Synopsis "
$obsFunctionName : $requestType
" -Description @"
$($obsRequestInfo.description)
$obsFunctionName calls the OBS WebSocket with a request of type $requestType.
"@ -Example @(
if ($obsRequestInfo.requestFields.Count -eq 0) {
"$obsFunctionName"
}
) -Link @(
"https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#$($requestType.ToLower())"
)
# If no function was created, something went wrong
if (-not $newFunc) {
# so leave this no-op in here to be able to easily debug.
$null = $null
} else {
# Otherwise, write the ful
$outputPath = Join-Path $requestsPath "$obsFunctionName.ps1"
$newFunc | Set-Content $outputPath -Encoding utf8
$builtFile = Get-Item -LiteralPath $outputPath
# and attach information about what was generated (in case of collision)
$builtFile |
Add-Member NoteProperty OBSRequestType $obsRequestInfo.requestType -Force -PassThru |
Add-Member NoteProperty OBSRequestInfo $obsRequestInfo -Force -PassThru |
Add-Member NoteProperty Contents "$newFunc" -Force
$filesBuilt += $builtFile
$obsFunctions += $newFunc
}
}
# Now group all the files
$filesBuilt |
Group-Object |
ForEach-Object {
# If it only created it once
if ($_.Count -eq 1) {
return $_.Group # return it directly
}
# Otherwise, basically do a simplified rename on each file
$groupOfDuplicates = $_.Group
$groupInfo = $_
# start by removing our current name
Remove-Item $groupOfDuplicates[0].FullName
# Then, for each duplicate
foreach ($file in $groupOfDuplicates) {
# Figure out the name it thought it had
$functionName = $file.Name -replace '\.ps1$'
# And the underlying request type
$requestType = $file.OBSRequestType
# use that to re-determine the function name
$newFunctionName =
if ($requestType -match $startsWithVerbRegex) {
$verbReplacements[$matches.0] + '-' +
$modulePrefix + ($RequestType -replace $startsWithVerbRegex)
} else {
"Send-${modulePrefix}${RequestType}"
}
# and use that name to determine a new path
$newPath = Join-Path $file.Directory "$newFunctionName.ps1"
# Then replace the function name within it's contents.
$file.contents -replace $functionName, $newFunctionName |
Set-Content -LiteralPath $newPath -Encoding utf8
# And return the new file, with the same information set attached.
Get-Item -LiteralPath $newPath |
Add-Member NoteProperty OBSRequestType $file.requestType -Force -PassThru |
Add-Member NoteProperty OBSRequestInfo $file.obsRequestInfo -Force -PassThru |
Add-Member NoteProperty Contents $file.Contents -Force -PassThru
}
}