discstation 0.1.18 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # Read an audio CD's table of contents via IMAPI2 raw reader. Emits JSON:
2
+ # {"track_count":N,"leadout":L,"tracks":[o1,o2,...]} (frame offsets, +150)
3
+ # Usage: audio-toc.ps1 <drive e.g. D:>
4
+ param([Parameter(Mandatory = $true)] [string] $Drive)
5
+
6
+ $ErrorActionPreference = "Stop"
7
+ . (Join-Path $PSScriptRoot "_json.ps1")
8
+
9
+ function Get-Recorder([string]$letter) {
10
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
11
+ for ($i = 0; $i -lt $master.Count; $i++) {
12
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
13
+ $rec.InitializeDiscRecorder($master.Item($i))
14
+ foreach ($p in $rec.VolumePathNames) { if ($p -and $p.TrimEnd('\') -ieq $letter) { return $rec } }
15
+ }
16
+ throw "No optical recorder for $letter"
17
+ }
18
+
19
+ $rec = Get-Recorder $Drive
20
+ $raw = New-Object -ComObject "IMAPI2.MsftDiscFormat2RawCD"
21
+ $raw.Recorder = $rec
22
+ $raw.ClientName = "DiscStation"
23
+
24
+ $toc = $raw.ReadDiscInformation() # not always present; fall through to raw TOC
25
+ $fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2Data"
26
+ $fmt.Recorder = $rec
27
+
28
+ # MsftDiscFormat2RawCD.get_TocInformation() -> byte array of the raw TOC (MMC-3).
29
+ $bytes = $raw.ReadTocInformation()
30
+ # TOC header: [0..1]=data length, [2]=first track, [3]=last track.
31
+ $first = $bytes[2]; $last = $bytes[3]
32
+ $offsets = @()
33
+ $leadout = 0
34
+ for ($i = 4; $i + 7 -lt $bytes.Length; $i += 8) {
35
+ $trk = $bytes[$i + 2]
36
+ # LBA is big-endian in bytes [i+4..i+7]
37
+ $lba = ($bytes[$i+4] -shl 24) -bor ($bytes[$i+5] -shl 16) -bor ($bytes[$i+6] -shl 8) -bor $bytes[$i+7]
38
+ if ($trk -eq 0xAA) { $leadout = $lba + 150 }
39
+ elseif ($trk -ge $first -and $trk -le $last) { $offsets += ($lba + 150) }
40
+ }
41
+ if ($offsets.Count -eq 0) { Write-Error "no audio tracks in TOC"; exit 2 }
42
+
43
+ Write-Output (ConvertTo-JsonCompat @{ track_count = $offsets.Count; leadout = $leadout; tracks = $offsets })
@@ -0,0 +1,87 @@
1
+ # Burn an audio CD (Red Book) from a folder of 16-bit / 44.1 kHz stereo WAV
2
+ # files via IMAPI2 Track-At-Once. Streams "PROGRESS:<pct>".
3
+ # Usage: burn-audio.ps1 <drive e.g. D:> <wav folder> [speed]
4
+ param(
5
+ [Parameter(Mandatory = $true)] [string] $Drive,
6
+ [Parameter(Mandatory = $true)] [string] $WavDir,
7
+ [string] $Speed = ""
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+ $wavs = @(Get-ChildItem -LiteralPath $WavDir -Filter *.wav | Sort-Object Name)
12
+ if ($wavs.Count -eq 0) { Write-Error "no WAV files in $WavDir"; exit 2 }
13
+
14
+ function Get-Recorder([string]$letter) {
15
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
16
+ for ($i = 0; $i -lt $master.Count; $i++) {
17
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
18
+ $rec.InitializeDiscRecorder($master.Item($i))
19
+ foreach ($p in $rec.VolumePathNames) {
20
+ if ($p -and $p.TrimEnd('\') -ieq $letter) { return $rec }
21
+ }
22
+ }
23
+ throw "No optical recorder for $letter"
24
+ }
25
+
26
+ $rec = Get-Recorder $Drive
27
+ $fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2TrackAtOnce"
28
+ if (-not $fmt.IsRecorderSupported($rec)) { Write-Error "recorder not supported"; exit 3 }
29
+ $fmt.Recorder = $rec
30
+ $fmt.ClientName = "DiscStation"
31
+ try { $fmt.NumberOfExistingTracks } catch {}
32
+ if ($Speed -and $Speed -match '^\d+') {
33
+ try { $fmt.SetWriteSpeed([int]($Speed -replace '\D',''), $false) } catch {}
34
+ }
35
+
36
+ $prepared = @()
37
+ foreach ($w in $wavs) {
38
+ # IMAPI2 wants raw 44100/16/2 PCM. Strip the 44-byte WAV header.
39
+ $bytes = [System.IO.File]::ReadAllBytes($w.FullName)
40
+ $offset = 44
41
+ $idx = -1
42
+ for ($i = 12; $i -lt [Math]::Min($bytes.Length - 8, 4096); $i++) {
43
+ if ($bytes[$i] -eq 0x64 -and $bytes[$i+1] -eq 0x61 -and $bytes[$i+2] -eq 0x74 -and $bytes[$i+3] -eq 0x61) {
44
+ $offset = $i + 8; break
45
+ }
46
+ }
47
+ $rawLen = $bytes.Length - $offset
48
+ # Red Book requires each track's byte length to be an exact multiple of
49
+ # the 2352-byte CD-DA sector - real-world track lengths essentially
50
+ # never land on that boundary naturally. AddAudioTrack rejects anything
51
+ # else outright ("The provided audio stream is not valid."). Pad with
52
+ # silence up to the next sector boundary (and up to the 4-second/
53
+ # 300-sector minimum a track must have) rather than trim real audio.
54
+ $sectorSize = 2352
55
+ $minLen = 300 * $sectorSize
56
+ $paddedLen = [Math]::Ceiling([Math]::Max($rawLen, $minLen) / $sectorSize) * $sectorSize
57
+ $raw = New-Object byte[] $paddedLen
58
+ [Array]::Copy($bytes, $offset, $raw, 0, $rawLen)
59
+ $prepared += ,@{ name = $w.Name; data = $raw }
60
+ }
61
+
62
+ $total = $prepared.Count
63
+ $done = 0
64
+ try {
65
+ # AddAudioTrack throws E_IMAPI_DF2TAO_MEDIA_IS_NOT_PREPARED ("only valid
66
+ # when media has been prepared") without this - PrepareMedia locks the
67
+ # drive for the write session, ReleaseMedia below hands it back.
68
+ $fmt.PrepareMedia()
69
+ foreach ($t in $prepared) {
70
+ $stream = New-Object -ComObject "ADODB.Stream"
71
+ $stream.Type = 1; $stream.Open()
72
+ $stream.Write($t.data)
73
+ $stream.Position = 0
74
+ $fmt.AddAudioTrack($stream)
75
+ $stream.Close()
76
+ $done++
77
+ Write-Output ("PROGRESS:" + [int]([math]::Min(99, $done * 100.0 / $total)))
78
+ }
79
+ $fmt.ReleaseMedia()
80
+ $fmt.Recorder.EjectMedia()
81
+ Write-Output "PROGRESS:100"
82
+ exit 0
83
+ } catch {
84
+ try { $fmt.ReleaseMedia() } catch {}
85
+ Write-Error ("audio burn failed: " + $_.Exception.Message)
86
+ exit 1
87
+ }
@@ -0,0 +1,83 @@
1
+ # Build a data-disc filesystem image from a folder (or single file) and burn it
2
+ # via IMAPI2 - no external mkisofs needed. Streams "PROGRESS:<pct>".
3
+ # Usage: burn-data.ps1 <drive e.g. D:> <source folder-or-file> <label> [speed]
4
+ param(
5
+ [Parameter(Mandatory = $true)] [string] $Drive,
6
+ [Parameter(Mandatory = $true)] [string] $Source,
7
+ [Parameter(Mandatory = $true)] [string] $Label,
8
+ [string] $Speed = ""
9
+ )
10
+
11
+ $ErrorActionPreference = "Stop"
12
+ if (-not (Test-Path $Source)) { Write-Error "source not found: $Source"; exit 2 }
13
+
14
+ function Get-Recorder([string]$letter) {
15
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
16
+ for ($i = 0; $i -lt $master.Count; $i++) {
17
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
18
+ $rec.InitializeDiscRecorder($master.Item($i))
19
+ foreach ($p in $rec.VolumePathNames) {
20
+ if ($p -and $p.TrimEnd('\') -ieq $letter) { return $rec }
21
+ }
22
+ }
23
+ throw "No optical recorder for $letter"
24
+ }
25
+
26
+ $rec = Get-Recorder $Drive
27
+ $fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2Data"
28
+ if (-not $fmt.IsRecorderSupported($rec)) { Write-Error "recorder not supported"; exit 3 }
29
+ $fmt.Recorder = $rec
30
+ $fmt.ClientName = "DiscStation"
31
+ # Without this the disc session never finalizes - the drive reports the
32
+ # disc as still blank afterward even though the data is physically there.
33
+ try { $fmt.ForceMediaToBeClosed = $true } catch {}
34
+ if ($Speed -and $Speed -match '^\d+') {
35
+ try { $fmt.SetWriteSpeed([int]($Speed -replace '\D',''), $false) } catch {}
36
+ }
37
+
38
+ # Filesystem image: ISO9660 + Joliet + UDF, sized to the loaded media.
39
+ $fsi = New-Object -ComObject "IMAPI2FS.MsftFileSystemImage"
40
+ try { $fsi.ChooseImageDefaultsForMediaType($fmt.CurrentPhysicalMediaType) } catch {}
41
+ $fsi.FileSystemsToCreate = 7 # ISO9660 | Joliet | UDF
42
+ $fsi.VolumeName = ($Label -replace '[^A-Za-z0-9_\- ]', '').Substring(0, [Math]::Min(32, ($Label -replace '[^A-Za-z0-9_\- ]', '').Length))
43
+ $fsi.FreeMediaBlocks = -1 # -1 = use the whole disc
44
+
45
+ $item = Get-Item -LiteralPath $Source
46
+ if ($item.PSIsContainer) {
47
+ # AddTree's 2nd arg is IncludeBaseDirectory: $false flattens a folder
48
+ # child into just its contents at the disc root (dropping the folder
49
+ # name entirely) - wrong for VIDEO_TS/AUDIO_TS or any subfolder, which
50
+ # need to keep their own name. $true preserves it as a real subfolder.
51
+ foreach ($child in Get-ChildItem -LiteralPath $Source) { $fsi.Root.AddTree($child.FullName, $true) }
52
+ } else {
53
+ $fsi.Root.AddTree($item.FullName, $false)
54
+ }
55
+
56
+ $result = $fsi.CreateResultImage()
57
+ $stream = $result.ImageStream
58
+
59
+ # Not fatal if registration fails (seen on some setups: "Cannot register
60
+ # for the specified event... does not exist") - the burn itself doesn't
61
+ # need it, just no live PROGRESS lines.
62
+ try {
63
+ Register-ObjectEvent -InputObject $fmt -EventName "Update" -SourceIdentifier "burn" -Action {
64
+ $s = $EventArgs
65
+ try {
66
+ $done = [double]$s.LastWrittenLba
67
+ $tot = [double]$s.SectorCount
68
+ if ($tot -gt 0) { Write-Output ("PROGRESS:" + [int]([math]::Min(99, $done * 100.0 / $tot))) }
69
+ } catch {}
70
+ } | Out-Null
71
+ } catch {}
72
+
73
+ try {
74
+ $fmt.Write($stream)
75
+ Write-Output "PROGRESS:100"
76
+ exit 0
77
+ } catch {
78
+ Write-Error ("burn failed: " + $_.Exception.Message)
79
+ exit 1
80
+ } finally {
81
+ Unregister-Event -SourceIdentifier "burn" -ErrorAction SilentlyContinue
82
+ try { $rec.EjectMedia() } catch {}
83
+ }
@@ -0,0 +1,68 @@
1
+ # Burn a pre-built ISO to the optical drive via IMAPI2. Streams "PROGRESS:<pct>"
2
+ # lines to stdout. Usage: burn-image.ps1 <drive e.g. D:> <iso path> [speed]
3
+ param(
4
+ [Parameter(Mandatory = $true)] [string] $Drive,
5
+ [Parameter(Mandatory = $true)] [string] $Iso,
6
+ [string] $Speed = ""
7
+ )
8
+
9
+ $ErrorActionPreference = "Stop"
10
+ if (-not (Test-Path $Iso)) { Write-Error "ISO not found: $Iso"; exit 2 }
11
+
12
+ function Get-Recorder([string]$letter) {
13
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
14
+ for ($i = 0; $i -lt $master.Count; $i++) {
15
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
16
+ $rec.InitializeDiscRecorder($master.Item($i))
17
+ foreach ($p in $rec.VolumePathNames) {
18
+ if ($p -and $p.TrimEnd('\') -ieq $letter) { return $rec }
19
+ }
20
+ }
21
+ throw "No optical recorder for $letter"
22
+ }
23
+
24
+ $rec = Get-Recorder $Drive
25
+ $fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2Data"
26
+ if (-not $fmt.IsRecorderSupported($rec)) { Write-Error "recorder not supported"; exit 3 }
27
+ $fmt.Recorder = $rec
28
+ $fmt.ClientName = "DiscStation"
29
+ try { $fmt.ForceMediaToBeClosed = $true } catch {}
30
+ if ($Speed -and $Speed -match '^\d+') {
31
+ try { $fmt.SetWriteSpeed([int]($Speed -replace '\D',''), $false) } catch {}
32
+ }
33
+
34
+ # Progress: IMAPI2 raises an Update event with sector counts. Not fatal if
35
+ # registration fails (seen on some setups: "Cannot register for the
36
+ # specified event... does not exist") - the burn itself doesn't need it,
37
+ # just no live PROGRESS lines.
38
+ try {
39
+ Register-ObjectEvent -InputObject $fmt -EventName "Update" -SourceIdentifier "burn" -Action {
40
+ $s = $EventArgs
41
+ try {
42
+ $done = [double]$s.LastWrittenLba
43
+ $tot = [double]$s.SectorCount
44
+ if ($tot -gt 0) {
45
+ $pct = [int]([math]::Min(99, $done * 100.0 / $tot))
46
+ Write-Output "PROGRESS:$pct"
47
+ }
48
+ } catch {}
49
+ } | Out-Null
50
+ } catch {}
51
+
52
+ $stream = New-Object -ComObject "ADODB.Stream"
53
+ $stream.Type = 1 # binary
54
+ $stream.Open()
55
+ $stream.LoadFromFile($Iso)
56
+
57
+ try {
58
+ $fmt.Write($stream)
59
+ Write-Output "PROGRESS:100"
60
+ exit 0
61
+ } catch {
62
+ Write-Error ("burn failed: " + $_.Exception.Message)
63
+ exit 1
64
+ } finally {
65
+ $stream.Close()
66
+ Unregister-Event -SourceIdentifier "burn" -ErrorAction SilentlyContinue
67
+ try { $rec.EjectMedia() } catch {}
68
+ }
@@ -0,0 +1,81 @@
1
+ # Optical drive + media state for DiscStation. Emits one JSON line.
2
+ # Works on PowerShell 2.0 (Win7) and later. Optional arg: a drive letter ("D:")
3
+ # to force; otherwise the first optical drive is used.
4
+ param([string]$Drive = "")
5
+
6
+ $ErrorActionPreference = "Stop"
7
+ . (Join-Path $PSScriptRoot "_json.ps1")
8
+
9
+ $out = @{ drive = ""; media_loaded = $false; blank = $false; label = "";
10
+ fs = ""; media_type = ""; rewritable = $false; capacity_bytes = 0 }
11
+
12
+ try {
13
+ $cd = @(Get-WmiObject Win32_CDROMDrive)
14
+ if ($Drive) { $cd = @($cd | Where-Object { $_.Drive -eq $Drive }) }
15
+ if ($cd.Count -eq 0) { Write-Output (ConvertTo-JsonCompat $out); exit 0 }
16
+ $d = $cd[0]
17
+ $out.drive = $d.Drive
18
+ # NOTE: [bool]"False" is $true in PowerShell (any non-empty string casts
19
+ # truthy) -- compare explicitly instead of casting.
20
+ $out.media_loaded = ($d.MediaLoaded -eq $true) -or ("$($d.MediaLoaded)" -eq "True")
21
+ } catch { Write-Output (ConvertTo-JsonCompat $out); exit 0 }
22
+
23
+ if (-not $out.media_loaded) { Write-Output (ConvertTo-JsonCompat $out); exit 0 }
24
+
25
+ # Volume label + filesystem (WMI logical disk).
26
+ $isAudioCd = $false
27
+ try {
28
+ $ld = Get-WmiObject Win32_LogicalDisk -Filter ("DeviceID='" + $out.drive + "'")
29
+ if ($ld) {
30
+ if ($ld.VolumeName) { $out.label = $ld.VolumeName }
31
+ # Windows presents a synthetic CDFS view (fixed volume name "Audio
32
+ # CD") for audio discs, purely so Explorer can browse track01.cda
33
+ # files - it's not a real filesystem. Treating it as one made every
34
+ # audio CD misclassify as a data disc.
35
+ if ($ld.VolumeName -eq "Audio CD" -and "$($ld.FileSystem)" -ieq "CDFS") {
36
+ $isAudioCd = $true
37
+ } elseif ($ld.FileSystem) {
38
+ $fs = $ld.FileSystem.ToLower()
39
+ if ($fs -match "udf") { $out.fs = "udf" }
40
+ elseif ($fs -match "cdfs|iso9660") { $out.fs = "iso9660" }
41
+ else { $out.fs = $fs }
42
+ }
43
+ if ($ld.Size) { $out.capacity_bytes = [int64]$ld.Size }
44
+ }
45
+ } catch {}
46
+
47
+ # IMAPI2: physical media type, blank flag, recordable capacity.
48
+ try {
49
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
50
+ for ($i = 0; $i -lt $master.Count; $i++) {
51
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
52
+ $rec.InitializeDiscRecorder($master.Item($i))
53
+ $match = $false
54
+ foreach ($p in $rec.VolumePathNames) { if ($p -and $p.TrimEnd('\') -ieq $out.drive) { $match = $true } }
55
+ if (-not $match) { continue }
56
+ $fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2Data"
57
+ if (-not $fmt.IsRecorderSupported($rec)) { break }
58
+ $fmt.Recorder = $rec
59
+ $fmt.ClientName = "DiscStation"
60
+ try { $out.blank = ($fmt.MediaHeuristicallyBlank -eq $true) } catch {}
61
+ try { if ($fmt.MediaPhysicallyBlank) { $out.blank = $true } } catch {}
62
+ try { $out.capacity_bytes = [int64]$fmt.TotalSectorsOnMedia * 2048 } catch {}
63
+ $t = 0; try { $t = [int]$fmt.CurrentPhysicalMediaType } catch {}
64
+ # IMAPI_MEDIA_PHYSICAL_TYPE
65
+ $map = @{ 1="cd-rom"; 2="cd-r"; 3="cd-rw"; 4="dvd-rom"; 5="dvd-r"; 6="dvd-ram";
66
+ 7="dvd+r"; 8="dvd+rw"; 9="dvd+r dl"; 10="dvd-r dl"; 12="dvd+rw dl";
67
+ 16="bd-rom"; 17="bd-r"; 18="bd-re" }
68
+ if ($map.ContainsKey($t)) { $out.media_type = $map[$t] }
69
+ if ($out.media_type -match "rw|ram|-re") { $out.rewritable = $true }
70
+ break
71
+ }
72
+ } catch {}
73
+
74
+ # $isAudioCd (Windows' own synthetic "Audio CD" CDFS view, detected above) is
75
+ # authoritative - overrides whatever IMAPI2's physical-media-type guess said,
76
+ # since a finalized audio CD-R still reports as generic "cd-rom" there.
77
+ if ($isAudioCd) {
78
+ $out.media_type = "audio_cd"
79
+ }
80
+
81
+ Write-Output (ConvertTo-JsonCompat $out)
@@ -0,0 +1,30 @@
1
+ # Eject or close the optical tray via IMAPI2, with a Shell.Application fallback.
2
+ # Usage: eject.ps1 <drive letter e.g. D:> [close]
3
+ param([string]$Drive = "", [switch]$Close)
4
+
5
+ $ErrorActionPreference = "Stop"
6
+ $ok = $false
7
+
8
+ try {
9
+ $master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
10
+ for ($i = 0; $i -lt $master.Count; $i++) {
11
+ $rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
12
+ $rec.InitializeDiscRecorder($master.Item($i))
13
+ $match = -not $Drive
14
+ foreach ($p in $rec.VolumePathNames) { if ($p -and $Drive -and $p.TrimEnd('\') -ieq $Drive) { $match = $true } }
15
+ if (-not $match) { continue }
16
+ if ($Close) { $rec.CloseTray() } else { $rec.EjectMedia() }
17
+ $ok = $true
18
+ break
19
+ }
20
+ } catch {}
21
+
22
+ if (-not $ok -and -not $Close -and $Drive) {
23
+ try {
24
+ $sh = New-Object -ComObject "Shell.Application"
25
+ $sh.Namespace(17).ParseName($Drive).InvokeVerb("Eject")
26
+ $ok = $true
27
+ } catch {}
28
+ }
29
+
30
+ if ($ok) { exit 0 } else { Write-Error "eject failed"; exit 1 }
@@ -0,0 +1,94 @@
1
+ # Play a Red Book audio CD via Windows Media Player's COM control - mpv on
2
+ # Windows has no libcdio, so cdda:// is "disabled at compile-time" there.
3
+ # WMPlayer.OCX only actually plays when hosted in a real window with a
4
+ # message pump; a bare `New-Object -ComObject` never leaves playState
5
+ # "Ready". Runs as a tiny persistent process, polled by the caller:
6
+ #
7
+ # Commands: caller writes one command to $CmdFile (PAUSE | STOP | NEXT |
8
+ # PREV | VOL:<0-100>); this script deletes it once consumed. A file-based
9
+ # channel instead of stdin - a background thread reading Console stdin
10
+ # here crashed the whole process outright (a piped stdin under a
11
+ # powershell.exe launched via -File from a console-less pythonw.exe
12
+ # parent apparently isn't safe to read from a second thread).
13
+ # Status (stdout): TRACK:<0-based index> | DONE | ERROR:<message>
14
+ #
15
+ # Usage: play-audio-cd.ps1 <drive e.g. D:> <command file path>
16
+ param(
17
+ [Parameter(Mandatory = $true)] [string] $Drive,
18
+ [Parameter(Mandatory = $true)] [string] $CmdFile
19
+ )
20
+
21
+ $ErrorActionPreference = "Stop"
22
+ Add-Type -AssemblyName System.Windows.Forms
23
+
24
+ # Write-Output through a redirected/piped stdout (SSH, or Python's own
25
+ # subprocess.PIPE) buffers until the process exits instead of flushing
26
+ # per line - confirmed live (only the final "DONE" ever arrived; every
27
+ # "TRACK:" sent during actual playback was stuck in the buffer). Writing
28
+ # straight to the console stream and flushing after each line sidesteps
29
+ # PowerShell's own output-pipeline buffering.
30
+ function Send-Status([string]$msg) {
31
+ [Console]::Out.WriteLine($msg)
32
+ [Console]::Out.Flush()
33
+ }
34
+
35
+ try {
36
+ $form = New-Object System.Windows.Forms.Form
37
+ $form.Show()
38
+ [System.Windows.Forms.Application]::DoEvents()
39
+
40
+ $wmp = New-Object -ComObject WMPlayer.OCX.7
41
+ $drives = $wmp.cdromCollection
42
+ $target = $null
43
+ for ($i = 0; $i -lt $drives.count; $i++) {
44
+ if ($drives.Item($i).driveSpecifier.TrimEnd('\') -ieq $Drive.TrimEnd('\')) { $target = $drives.Item($i); break }
45
+ }
46
+ if (-not $target) { Send-Status "ERROR:No CD drive $Drive"; exit 2 }
47
+
48
+ $wmp.currentPlaylist = $target.playlist
49
+ $wmp.controls.play()
50
+ } catch {
51
+ Send-Status ("ERROR:" + $_.Exception.Message)
52
+ exit 1
53
+ }
54
+
55
+ $lastTrack = -1
56
+ while ($true) {
57
+ Start-Sleep -Milliseconds 200
58
+ [System.Windows.Forms.Application]::DoEvents()
59
+
60
+ if ($wmp.playState -eq 8) { Send-Status "DONE"; break } # wmppsMediaEnded
61
+
62
+ # currentItem.playlistIndex comes back empty on this drive/build -
63
+ # confirmed live - but .name reliably reads "Track N" for a CD
64
+ # playlist item, so parse the number out of that instead.
65
+ $track = -1
66
+ try {
67
+ $item = $wmp.controls.currentItem
68
+ if ($item -and $item.name -match 'Track\s+(\d+)') { $track = [int]$Matches[1] - 1 }
69
+ } catch {}
70
+ if ($track -ge 0 -and $track -ne $lastTrack) {
71
+ $lastTrack = $track
72
+ Send-Status "TRACK:$track"
73
+ }
74
+
75
+ if (Test-Path -LiteralPath $CmdFile) {
76
+ $line = (Get-Content -LiteralPath $CmdFile -Raw -ErrorAction SilentlyContinue)
77
+ Remove-Item -LiteralPath $CmdFile -ErrorAction SilentlyContinue
78
+ if ($line) {
79
+ $line = $line.Trim()
80
+ switch -Regex ($line) {
81
+ '^PAUSE$' {
82
+ if ($wmp.playState -eq 3) { $wmp.controls.pause() } else { $wmp.controls.play() }
83
+ }
84
+ '^STOP$' { $wmp.controls.stop(); Send-Status "DONE"; break }
85
+ '^NEXT$' { $wmp.controls.next() }
86
+ '^PREV$' { $wmp.controls.previous() }
87
+ '^VOL:(\d+)$' { $wmp.settings.volume = [Math]::Min(100, [Math]::Max(0, [int]$Matches[1])) }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ try { $wmp.controls.stop(); $wmp.close() } catch {}
94
+ try { $form.Close() } catch {}