cd.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. # Copyright © 2024 Noah Vogt <noah@noahvogt.com>
  2. # This program is free software: you can redistribute it and/or modify
  3. # it under the terms of the GNU General Public License as published by
  4. # the Free Software Foundation, either version 3 of the License, or
  5. # (at your option) any later version.
  6. # This program is distributed in the hope that it will be useful,
  7. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  8. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  9. # GNU General Public License for more details.
  10. # You should have received a copy of the GNU General Public License
  11. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  12. import sys
  13. import os
  14. from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module
  15. QApplication,
  16. QMessageBox,
  17. QDialog,
  18. )
  19. from utils import log, CustomException, RadioButtonDialog
  20. if os.name == "nt":
  21. # pylint: disable=import-error
  22. import wmi # pyright: ignore
  23. import ctypes
  24. from subprocess import PIPE, run
  25. from re import match, search
  26. import config as const
  27. else:
  28. from pycdio import DRIVER_DEVICE
  29. from cdio import (
  30. get_devices,
  31. DriverUnsupportedError,
  32. DeviceException,
  33. )
  34. def choose_right_scsi_drive(drives: list) -> str:
  35. if len(drives) != 1:
  36. log("Warning: More than one SCSI drive found", color="yellow")
  37. if (
  38. # pylint: disable=possibly-used-before-assignment
  39. const.CD_RECORD_PREFERED_SCSI_DRIVE in drives
  40. and const.CD_RECORD_PREFERED_SCSI_DRIVE != ""
  41. ):
  42. return const.CD_RECORD_PREFERED_SCSI_DRIVE
  43. # pylint: disable=possibly-used-before-assignment
  44. dialog = RadioButtonDialog(drives, "Choose a SCSI Drive")
  45. if dialog.exec_() == QDialog.Accepted:
  46. log(f"Dialog accepted: {dialog.chosen}")
  47. return dialog.chosen
  48. log("Warning: Choosing first SCSI drive...", color="yellow")
  49. return drives[0]
  50. def get_cdrecord_devname(cd_drive: str) -> str:
  51. if os.name == "nt":
  52. scsi_drives = get_scsi_drives()
  53. return choose_right_scsi_drive(scsi_drives)
  54. return cd_drive
  55. def bytes_line_ends_with(line: bytes, target: str) -> bool:
  56. byte_len = len(line)
  57. if byte_len < len(target):
  58. return False
  59. if len(target) == 1 and ord(target[0]) != line[-1]:
  60. return False
  61. for index in range(-1, -1 * len(target), -1):
  62. if ord(target[index]) != line[index]:
  63. return False
  64. return True
  65. def get_scsi_drives() -> list[str]:
  66. # pylint: disable=possibly-used-before-assignment, subprocess-run-check
  67. try:
  68. drives = []
  69. result = run(["cdrecord", "-scanbus"], stdout=PIPE)
  70. output = result.stdout
  71. if result.returncode != 0:
  72. raise CustomException("Command 'cdrecord -scanbus' failed.")
  73. for line in output.split(b"\n"):
  74. # pylint: disable=possibly-used-before-assignment
  75. if (
  76. match(rb"\s*[0-9](,[0-9])+\s*[0-9]+", line)
  77. and (not bytes_line_ends_with(line, "*"))
  78. and (not bytes_line_ends_with(line, "HOST ADAPTOR"))
  79. ):
  80. matches = search(rb"[0-9](,[0-9])+", line)
  81. if matches is None:
  82. raise CustomException(f"could not parse line: '{line}'")
  83. drives.append(matches.group().decode("ascii"))
  84. if not drives:
  85. raise CustomException("Could not find any SCSI drives")
  86. return drives
  87. except CustomException as error:
  88. app = QApplication([])
  89. QMessageBox.critical(
  90. None,
  91. "Error",
  92. f"Could not get SCSI drives. Reason: {error}",
  93. )
  94. del app
  95. sys.exit(1)
  96. def get_cd_drives() -> list[str]:
  97. if os.name == "nt":
  98. c = wmi.WMI()
  99. cd_drives = []
  100. for cd in c.Win32_CDROMDrive():
  101. cd_drives.append(cd.Drive)
  102. # pylint: disable=possibly-used-before-assignment
  103. else:
  104. raw_cd_drives = get_devices(DRIVER_DEVICE)
  105. cd_drives = []
  106. for cd in raw_cd_drives:
  107. cd_drives.append(str(cd.get_device()))
  108. return cd_drives
  109. # pylint: disable=possibly-used-before-assignment
  110. def eject_drive(drive: str) -> None: # pyright: ignore
  111. if os.name == "nt":
  112. ctypes.windll.WINMM.mciSendStringW(
  113. f"open {drive} type cdaudio alias d_drive", None, 0, None
  114. )
  115. ctypes.windll.WINMM.mciSendStringW(
  116. "set d_drive door open", None, 0, None
  117. )
  118. else:
  119. try:
  120. raw_drives = get_devices(DRIVER_DEVICE)
  121. drive_ejected = False
  122. for cd in raw_drives:
  123. if str(cd.get_device()) == drive:
  124. drive.eject_media() # pyright: ignore
  125. drive_ejected = True
  126. if not drive_ejected:
  127. raise CustomException(f"Drive {drive} not found")
  128. # pylint: disable=possibly-used-before-assignment
  129. except (DriverUnsupportedError, DeviceException, CustomException):
  130. log(f"Eject of CD-ROM drive {drive} failed") # pyright: ignore