discstation 0.1.35 → 0.1.37
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.
- package/README.md +10 -7
- package/arduino/DiscStation/DiscStation.ino +13 -18
- package/discstation.env.example +1 -1
- package/docs/PLATFORM_SUPPORT.md +13 -31
- package/install-macos.sh +3 -6
- package/install.sh +2 -1
- package/package.json +1 -1
- package/requirements-optional.txt +5 -7
- package/scripts/check.sh +61 -0
- package/src/discstation.py +141 -707
- package/src/discstation_burn.py +18 -207
- package/src/static/style.css +0 -7
- package/src/win/_imapi.ps1 +66 -0
- package/src/win/audio-toc.ps1 +1 -9
- package/src/win/burn-audio.ps1 +2 -14
- package/src/win/burn-data.ps1 +4 -47
- package/src/win/burn-image.ps1 +4 -53
package/src/discstation_burn.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
import datetime
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
5
4
|
import re
|
|
6
5
|
import serial
|
|
7
6
|
import shutil
|
|
8
7
|
import subprocess
|
|
9
|
-
import sys
|
|
10
8
|
import threading
|
|
11
9
|
import time
|
|
12
10
|
import unicodedata
|
|
@@ -169,24 +167,6 @@ def reset_drive(device=None):
|
|
|
169
167
|
print("reset_drive: no matching USB optical device found to reset")
|
|
170
168
|
return False
|
|
171
169
|
|
|
172
|
-
def _esp32_port_from_sysfs():
|
|
173
|
-
"""Find the ESP32's USB serial port by matching VID/PID 303a:1001
|
|
174
|
-
in sysfs. Returns the device path (e.g. /dev/ttyACM0) or None."""
|
|
175
|
-
for tty in Path("/sys/class/tty").glob("ttyACM*"):
|
|
176
|
-
uevent = tty / "device" / "uevent"
|
|
177
|
-
if uevent.exists():
|
|
178
|
-
modalias = uevent.read_text()
|
|
179
|
-
if "303a/1001" in modalias or "303a:1001" in modalias:
|
|
180
|
-
dev = Path("/dev") / tty.name
|
|
181
|
-
if dev.exists():
|
|
182
|
-
return str(dev)
|
|
183
|
-
return None
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def detect_esp32_port():
|
|
187
|
-
return discstation_host.serial_port() or ""
|
|
188
|
-
|
|
189
|
-
PORT = detect_esp32_port()
|
|
190
170
|
BAUD = 115200
|
|
191
171
|
|
|
192
172
|
|
|
@@ -204,19 +184,26 @@ def check_cancel(ser):
|
|
|
204
184
|
return False
|
|
205
185
|
|
|
206
186
|
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
187
|
+
def proc_line_queue(proc):
|
|
188
|
+
"""Pump proc.stdout into a Queue from a daemon thread. Returns
|
|
189
|
+
(queue, done): `done` is the sentinel put on the queue at EOF."""
|
|
190
|
+
lines, done = Queue(), object()
|
|
210
191
|
|
|
211
|
-
def
|
|
192
|
+
def pump():
|
|
212
193
|
try:
|
|
213
194
|
for line in proc.stdout:
|
|
214
195
|
lines.put(line.rstrip("\r\n"))
|
|
215
196
|
finally:
|
|
216
|
-
lines.put(
|
|
197
|
+
lines.put(done)
|
|
198
|
+
|
|
199
|
+
threading.Thread(target=pump, daemon=True).start()
|
|
200
|
+
return lines, done
|
|
217
201
|
|
|
218
|
-
|
|
219
|
-
|
|
202
|
+
|
|
203
|
+
def iter_proc_or_cancel(proc, ser):
|
|
204
|
+
"""Yield proc's output lines, PINGing the remote every 5s; a CANCEL from
|
|
205
|
+
the remote stops the process and ends the iteration."""
|
|
206
|
+
lines, done = proc_line_queue(proc)
|
|
220
207
|
last_ping = time.time()
|
|
221
208
|
output_done = False
|
|
222
209
|
while proc.poll() is None or not output_done:
|
|
@@ -231,14 +218,12 @@ def iter_proc_or_cancel(proc, ser):
|
|
|
231
218
|
line = lines.get(timeout=0.2)
|
|
232
219
|
except Empty:
|
|
233
220
|
continue
|
|
234
|
-
if line is
|
|
221
|
+
if line is done:
|
|
235
222
|
output_done = True
|
|
236
223
|
else:
|
|
237
224
|
yield line
|
|
238
|
-
reader.join(timeout=1)
|
|
239
225
|
|
|
240
226
|
|
|
241
|
-
DVD_DEVICE = os.environ.get("DISC_DEVICE") or os.environ.get("DVD_DEVICE")
|
|
242
227
|
DISC_SPEED = os.environ.get("DISC_SPEED")
|
|
243
228
|
DISC_DISC_BYTES = 4_700_000_000
|
|
244
229
|
DVD_DL_BYTES = 8_500_000_000
|
|
@@ -1737,18 +1722,7 @@ def _run_windows_burn(ser, script, *script_args):
|
|
|
1737
1722
|
cmd, kwargs = discstation_host.ps_cmd(script, *script_args)
|
|
1738
1723
|
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, **kwargs)
|
|
1739
1724
|
|
|
1740
|
-
lines =
|
|
1741
|
-
finished = object()
|
|
1742
|
-
|
|
1743
|
-
def read_output():
|
|
1744
|
-
try:
|
|
1745
|
-
for line in proc.stdout:
|
|
1746
|
-
lines.put(line.rstrip("\r\n"))
|
|
1747
|
-
finally:
|
|
1748
|
-
lines.put(finished)
|
|
1749
|
-
|
|
1750
|
-
reader = threading.Thread(target=read_output, daemon=True)
|
|
1751
|
-
reader.start()
|
|
1725
|
+
lines, finished = proc_line_queue(proc)
|
|
1752
1726
|
|
|
1753
1727
|
out_lines, last_pct, last_real_progress, start = [], -1, 0.0, time.time()
|
|
1754
1728
|
last_ping, output_done = time.time(), False
|
|
@@ -1784,7 +1758,6 @@ def _run_windows_burn(ser, script, *script_args):
|
|
|
1784
1758
|
if est_pct > last_pct:
|
|
1785
1759
|
last_pct = est_pct
|
|
1786
1760
|
send(ser, f"PROGRESS:{est_pct}%")
|
|
1787
|
-
reader.join(timeout=1)
|
|
1788
1761
|
except (KeyboardInterrupt, SystemExit):
|
|
1789
1762
|
stop_process(proc)
|
|
1790
1763
|
raise
|
|
@@ -1841,10 +1814,8 @@ def remux_and_author(ser, mpg, disc_label, disc_capacity, dvd_aspect=None):
|
|
|
1841
1814
|
silent blocking subprocess call that lets the ESP32's 30s watchdog
|
|
1842
1815
|
flip the display to "disconnected" partway through.
|
|
1843
1816
|
|
|
1844
|
-
This is the single place the remux+author sequence lives
|
|
1845
|
-
|
|
1846
|
-
any CLI entry point all call this (via remux_and_burn below, or
|
|
1847
|
-
directly), so a fix here only has to happen once.
|
|
1817
|
+
This is the single place the remux+author sequence lives (burn_flow
|
|
1818
|
+
calls it after the download/convert pipeline).
|
|
1848
1819
|
"""
|
|
1849
1820
|
check_encoded_size(ser, mpg, disc_capacity)
|
|
1850
1821
|
send(ser, f"TITLE:{disc_label}")
|
|
@@ -1894,163 +1865,3 @@ def remux_and_author(ser, mpg, disc_label, disc_capacity, dvd_aspect=None):
|
|
|
1894
1865
|
dvd_dir = author(ser, fixed, fixed.parent, dvd_aspect)
|
|
1895
1866
|
check_dvd_size(ser, dvd_dir, disc_capacity)
|
|
1896
1867
|
return dvd_dir
|
|
1897
|
-
|
|
1898
|
-
def remux_and_burn(ser, mpg, disc_label, disc_capacity, dl_info, burn_speed=None, dvd_aspect=None):
|
|
1899
|
-
dvd_dir = remux_and_author(ser, mpg, disc_label, disc_capacity, dvd_aspect)
|
|
1900
|
-
wait_for_burn_confirm(ser, dvd_dir, disc_capacity)
|
|
1901
|
-
burn(ser, dvd_dir, disc_label, burn_speed, dl_info["is_dual_layer"])
|
|
1902
|
-
safe_send(ser, "DONE:Burn complete!")
|
|
1903
|
-
print(f"Burned {mpg} as {disc_label}")
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
def main():
|
|
1907
|
-
if len(sys.argv) < 2:
|
|
1908
|
-
print("Usage: python3 discstation_burn.py 'video URL or file path'")
|
|
1909
|
-
sys.exit(1)
|
|
1910
|
-
url = sys.argv[1]
|
|
1911
|
-
WORK.mkdir(parents=True, exist_ok=True)
|
|
1912
|
-
job_dir = WORK / time.strftime("job_%Y%m%d_%H%M%S")
|
|
1913
|
-
job_dir.mkdir()
|
|
1914
|
-
ser = None
|
|
1915
|
-
try:
|
|
1916
|
-
ser = serial.Serial(PORT, BAUD, timeout=1)
|
|
1917
|
-
reset_serial_state()
|
|
1918
|
-
time.sleep(2)
|
|
1919
|
-
print("Connected to DiscStation")
|
|
1920
|
-
print("Running preflight...")
|
|
1921
|
-
send(ser, "STATUS:Preflight...")
|
|
1922
|
-
info = get_video_info(url, ser)
|
|
1923
|
-
title = info["title"]
|
|
1924
|
-
duration = info["duration"]
|
|
1925
|
-
duration_line, fit_line, can_fit = preflight_lines(duration)
|
|
1926
|
-
print(f"Title: {title}")
|
|
1927
|
-
print(f"Duration: {format_duration(duration)}")
|
|
1928
|
-
print(f"Preflight: {fit_line}")
|
|
1929
|
-
print(f"DVD drive: {disc_device()}")
|
|
1930
|
-
disc_label = sanitize_disc_label(title)
|
|
1931
|
-
print(f"Disc label: {disc_label}")
|
|
1932
|
-
send(ser, f"TITLE:{title}")
|
|
1933
|
-
send(ser, f"META:{duration_line}")
|
|
1934
|
-
send(ser, f"FIT:{fit_line}")
|
|
1935
|
-
if not can_fit:
|
|
1936
|
-
raise RuntimeError("Video too long for DVD5")
|
|
1937
|
-
|
|
1938
|
-
device = disc_device()
|
|
1939
|
-
dl_info = detect_disc_type(device)
|
|
1940
|
-
if dl_info["is_dual_layer"]:
|
|
1941
|
-
sl_target = int(os.environ.get("DISC_TARGET_BYTES", "4300000000"))
|
|
1942
|
-
try:
|
|
1943
|
-
sl_plan = bitrate_plan(duration, "AUTO", sl_target)
|
|
1944
|
-
if sl_plan:
|
|
1945
|
-
warn = f"DL disc for SL content"
|
|
1946
|
-
print(f"WARNING: {warn}")
|
|
1947
|
-
safe_send(ser, f"WARNING:{warn}")
|
|
1948
|
-
time.sleep(3)
|
|
1949
|
-
except RuntimeError:
|
|
1950
|
-
pass
|
|
1951
|
-
|
|
1952
|
-
print("Waiting for button press...")
|
|
1953
|
-
selected_mode = "AUTO"
|
|
1954
|
-
burn_speed = None
|
|
1955
|
-
while True:
|
|
1956
|
-
if ser.in_waiting:
|
|
1957
|
-
resp = ser.readline().decode(errors='ignore').strip()
|
|
1958
|
-
note_serial_activity()
|
|
1959
|
-
if resp == "CANCEL":
|
|
1960
|
-
print("Cancelled by user")
|
|
1961
|
-
safe_send(ser, "CANCELLED:Cancelled")
|
|
1962
|
-
sys.exit(130)
|
|
1963
|
-
elif resp.startswith("MODE:"):
|
|
1964
|
-
selected_mode = normalize_mode(resp.split(":", 1)[1])
|
|
1965
|
-
print(f"Mode: {selected_mode}")
|
|
1966
|
-
elif resp.startswith("SPEED:"):
|
|
1967
|
-
burn_speed = resp.split(":", 1)[1].strip()
|
|
1968
|
-
print(f"Burn speed: {burn_speed}")
|
|
1969
|
-
elif resp == "START" or resp.startswith("START:"):
|
|
1970
|
-
if ":" in resp:
|
|
1971
|
-
selected_mode = normalize_mode(resp.split(":", 1)[1])
|
|
1972
|
-
print(f"Button pressed - starting in {selected_mode} mode!")
|
|
1973
|
-
send(ser, f"STATUS:Starting {selected_mode}...")
|
|
1974
|
-
break
|
|
1975
|
-
time.sleep(0.1)
|
|
1976
|
-
|
|
1977
|
-
start_time = time.time()
|
|
1978
|
-
disc_type_label = "DL" if dl_info["is_dual_layer"] else "SL"
|
|
1979
|
-
try:
|
|
1980
|
-
disc_bytes = dl_info["capacity"]
|
|
1981
|
-
plan = bitrate_plan(duration, selected_mode, disc_bytes)
|
|
1982
|
-
video = download(ser, url, job_dir)
|
|
1983
|
-
mpg, dvd_aspect = convert(ser, video, job_dir, selected_mode, disc_bytes)
|
|
1984
|
-
dvd = remux_and_author(ser, mpg, disc_label, disc_bytes, dvd_aspect)
|
|
1985
|
-
if plan["burn"]:
|
|
1986
|
-
burn(ser, dvd, disc_label, burn_speed, dl_info["is_dual_layer"])
|
|
1987
|
-
send(ser, "DONE:Disc complete!")
|
|
1988
|
-
print("Done!")
|
|
1989
|
-
else:
|
|
1990
|
-
send(ser, "DONE:Test complete!")
|
|
1991
|
-
print("Test complete. DVD folder was built but not burned.")
|
|
1992
|
-
append_history({
|
|
1993
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
|
1994
|
-
"title": title,
|
|
1995
|
-
"disc_type": disc_type_label,
|
|
1996
|
-
"mode": selected_mode,
|
|
1997
|
-
"speed": burn_speed or "Auto",
|
|
1998
|
-
"success": True,
|
|
1999
|
-
"duration_s": round(time.time() - start_time),
|
|
2000
|
-
})
|
|
2001
|
-
except (KeyboardInterrupt, SystemExit):
|
|
2002
|
-
append_history({
|
|
2003
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
|
2004
|
-
"title": title,
|
|
2005
|
-
"disc_type": disc_type_label,
|
|
2006
|
-
"mode": selected_mode,
|
|
2007
|
-
"speed": burn_speed or "Auto",
|
|
2008
|
-
"success": False,
|
|
2009
|
-
"error": "Cancelled",
|
|
2010
|
-
"duration_s": round(time.time() - start_time),
|
|
2011
|
-
})
|
|
2012
|
-
raise
|
|
2013
|
-
except Exception as e:
|
|
2014
|
-
append_history({
|
|
2015
|
-
"timestamp": datetime.datetime.now().isoformat(),
|
|
2016
|
-
"title": title,
|
|
2017
|
-
"disc_type": disc_type_label,
|
|
2018
|
-
"mode": selected_mode,
|
|
2019
|
-
"speed": burn_speed or "Auto",
|
|
2020
|
-
"success": False,
|
|
2021
|
-
"error": str(e)[:100],
|
|
2022
|
-
"duration_s": round(time.time() - start_time),
|
|
2023
|
-
})
|
|
2024
|
-
raise
|
|
2025
|
-
except (KeyboardInterrupt, SystemExit):
|
|
2026
|
-
safe_send(ser, "CANCELLED:Stopped")
|
|
2027
|
-
print("Cancelled")
|
|
2028
|
-
sys.exit(130)
|
|
2029
|
-
except RuntimeError as e:
|
|
2030
|
-
msg = str(e)
|
|
2031
|
-
if msg == "Cancelled":
|
|
2032
|
-
safe_send(ser, "CANCELLED:Stopped")
|
|
2033
|
-
else:
|
|
2034
|
-
safe_send(ser, f"ERROR:{msg[:20]}")
|
|
2035
|
-
print(f"Error: {e}")
|
|
2036
|
-
sys.exit(1)
|
|
2037
|
-
except Exception as e:
|
|
2038
|
-
safe_send(ser, f"ERROR:{str(e)[:20]}")
|
|
2039
|
-
print(f"Error: {e}")
|
|
2040
|
-
sys.exit(1)
|
|
2041
|
-
finally:
|
|
2042
|
-
if ser:
|
|
2043
|
-
ser.close()
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
BURN_HISTORY = WORK / "burn_history.jsonl"
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
def append_history(entry):
|
|
2050
|
-
BURN_HISTORY.parent.mkdir(parents=True, exist_ok=True)
|
|
2051
|
-
with open(BURN_HISTORY, "a") as f:
|
|
2052
|
-
f.write(json.dumps(entry) + "\n")
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
if __name__ == "__main__":
|
|
2056
|
-
main()
|
package/src/static/style.css
CHANGED
|
@@ -177,13 +177,6 @@ h1 {
|
|
|
177
177
|
font-weight: 900;
|
|
178
178
|
}
|
|
179
179
|
h1 em { font-style: normal; -webkit-text-stroke: 1px var(--ink); color: transparent; }
|
|
180
|
-
.lede {
|
|
181
|
-
max-width: 560px;
|
|
182
|
-
margin: 28px 0 26px;
|
|
183
|
-
color: var(--muted);
|
|
184
|
-
font-size: 12px;
|
|
185
|
-
line-height: 1.8;
|
|
186
|
-
}
|
|
187
180
|
.hero-specs {
|
|
188
181
|
display: flex;
|
|
189
182
|
flex-wrap: wrap;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Shared IMAPI2 helpers, dot-sourced by the burn scripts (same pattern as _json.ps1).
|
|
2
|
+
|
|
3
|
+
function Get-Recorder([string]$letter) {
|
|
4
|
+
$master = New-Object -ComObject "IMAPI2.MsftDiscMaster2"
|
|
5
|
+
for ($i = 0; $i -lt $master.Count; $i++) {
|
|
6
|
+
$rec = New-Object -ComObject "IMAPI2.MsftDiscRecorder2"
|
|
7
|
+
$rec.InitializeDiscRecorder($master.Item($i))
|
|
8
|
+
foreach ($p in $rec.VolumePathNames) {
|
|
9
|
+
if ($p -and $p.TrimEnd('\') -ieq $letter) { return $rec }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
throw "No optical recorder for $letter"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Apply an optional "8x"-style write speed; not fatal if the drive refuses it.
|
|
16
|
+
function Set-WriteSpeed($fmt, [string]$speed) {
|
|
17
|
+
if ($speed -and $speed -match '^\d+') {
|
|
18
|
+
try { $fmt.SetWriteSpeed([int]($speed -replace '\D',''), $false) } catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# A data-disc (IMAPI2.MsftDiscFormat2Data) writer bound to the drive letter.
|
|
23
|
+
function New-DataWriter([string]$letter, [string]$speed) {
|
|
24
|
+
$rec = Get-Recorder $letter
|
|
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
|
+
# Without this the disc session never finalizes - the drive reports the
|
|
30
|
+
# disc as still blank afterward even though the data is physically there.
|
|
31
|
+
try { $fmt.ForceMediaToBeClosed = $true } catch {}
|
|
32
|
+
Set-WriteSpeed $fmt $speed
|
|
33
|
+
return $fmt
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Write $stream to the disc, streaming "PROGRESS:<pct>" lines, then eject.
|
|
37
|
+
# Sets $global:BurnExit (0 ok, 1 failed) for the caller to `exit` with.
|
|
38
|
+
function Write-DiscStream($fmt, $stream) {
|
|
39
|
+
# IMAPI2 raises an Update event with sector counts. Not fatal if
|
|
40
|
+
# registration fails (seen on some setups: "Cannot register for the
|
|
41
|
+
# specified event... does not exist") - the burn itself doesn't need it,
|
|
42
|
+
# just no live PROGRESS lines.
|
|
43
|
+
try {
|
|
44
|
+
Register-ObjectEvent -InputObject $fmt -EventName "Update" -SourceIdentifier "burn" -Action {
|
|
45
|
+
$s = $EventArgs
|
|
46
|
+
try {
|
|
47
|
+
$done = [double]$s.LastWrittenLba
|
|
48
|
+
$tot = [double]$s.SectorCount
|
|
49
|
+
if ($tot -gt 0) { Write-Output ("PROGRESS:" + [int]([math]::Min(99, $done * 100.0 / $tot))) }
|
|
50
|
+
} catch {}
|
|
51
|
+
} | Out-Null
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
$fmt.Write($stream)
|
|
56
|
+
Write-Output "PROGRESS:100"
|
|
57
|
+
$global:BurnExit = 0
|
|
58
|
+
} catch {
|
|
59
|
+
Write-Error ("burn failed: " + $_.Exception.Message)
|
|
60
|
+
$global:BurnExit = 1
|
|
61
|
+
} finally {
|
|
62
|
+
try { $stream.Close() } catch {}
|
|
63
|
+
Unregister-Event -SourceIdentifier "burn" -ErrorAction SilentlyContinue
|
|
64
|
+
try { $fmt.Recorder.EjectMedia() } catch {}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/win/audio-toc.ps1
CHANGED
|
@@ -6,15 +6,7 @@ param([Parameter(Mandatory = $true)] [string] $Drive)
|
|
|
6
6
|
$ErrorActionPreference = "Stop"
|
|
7
7
|
. (Join-Path $PSScriptRoot "_json.ps1")
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
}
|
|
9
|
+
. (Join-Path $PSScriptRoot "_imapi.ps1")
|
|
18
10
|
|
|
19
11
|
$rec = Get-Recorder $Drive
|
|
20
12
|
$raw = New-Object -ComObject "IMAPI2.MsftDiscFormat2RawCD"
|
package/src/win/burn-audio.ps1
CHANGED
|
@@ -11,17 +11,7 @@ $ErrorActionPreference = "Stop"
|
|
|
11
11
|
$wavs = @(Get-ChildItem -LiteralPath $WavDir -Filter *.wav | Sort-Object Name)
|
|
12
12
|
if ($wavs.Count -eq 0) { Write-Error "no WAV files in $WavDir"; exit 2 }
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
}
|
|
14
|
+
. (Join-Path $PSScriptRoot "_imapi.ps1")
|
|
25
15
|
|
|
26
16
|
$rec = Get-Recorder $Drive
|
|
27
17
|
$fmt = New-Object -ComObject "IMAPI2.MsftDiscFormat2TrackAtOnce"
|
|
@@ -29,9 +19,7 @@ if (-not $fmt.IsRecorderSupported($rec)) { Write-Error "recorder not supported";
|
|
|
29
19
|
$fmt.Recorder = $rec
|
|
30
20
|
$fmt.ClientName = "DiscStation"
|
|
31
21
|
try { $fmt.NumberOfExistingTracks } catch {}
|
|
32
|
-
|
|
33
|
-
try { $fmt.SetWriteSpeed([int]($Speed -replace '\D',''), $false) } catch {}
|
|
34
|
-
}
|
|
22
|
+
Set-WriteSpeed $fmt $Speed
|
|
35
23
|
|
|
36
24
|
$prepared = @()
|
|
37
25
|
foreach ($w in $wavs) {
|
package/src/win/burn-data.ps1
CHANGED
|
@@ -11,29 +11,9 @@ param(
|
|
|
11
11
|
$ErrorActionPreference = "Stop"
|
|
12
12
|
if (-not (Test-Path $Source)) { Write-Error "source not found: $Source"; exit 2 }
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
}
|
|
14
|
+
. (Join-Path $PSScriptRoot "_imapi.ps1")
|
|
25
15
|
|
|
26
|
-
$
|
|
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
|
-
}
|
|
16
|
+
$fmt = New-DataWriter $Drive $Speed
|
|
37
17
|
|
|
38
18
|
# Filesystem image: ISO9660 + Joliet + UDF, sized to the loaded media.
|
|
39
19
|
$fsi = New-Object -ComObject "IMAPI2FS.MsftFileSystemImage"
|
|
@@ -56,28 +36,5 @@ if ($item.PSIsContainer) {
|
|
|
56
36
|
$result = $fsi.CreateResultImage()
|
|
57
37
|
$stream = $result.ImageStream
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
}
|
|
39
|
+
Write-DiscStream $fmt $stream
|
|
40
|
+
exit $global:BurnExit
|
package/src/win/burn-image.ps1
CHANGED
|
@@ -8,61 +8,12 @@ param(
|
|
|
8
8
|
|
|
9
9
|
$ErrorActionPreference = "Stop"
|
|
10
10
|
if (-not (Test-Path $Iso)) { Write-Error "ISO not found: $Iso"; exit 2 }
|
|
11
|
+
. (Join-Path $PSScriptRoot "_imapi.ps1")
|
|
11
12
|
|
|
12
|
-
|
|
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
|
-
|
|
13
|
+
$fmt = New-DataWriter $Drive $Speed
|
|
52
14
|
$stream = New-Object -ComObject "ADODB.Stream"
|
|
53
15
|
$stream.Type = 1 # binary
|
|
54
16
|
$stream.Open()
|
|
55
17
|
$stream.LoadFromFile($Iso)
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|
|
18
|
+
Write-DiscStream $fmt $stream
|
|
19
|
+
exit $global:BurnExit
|