cd.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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. print(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. for index in range(-1, -1 * len(target), -1):
  60. if ord(target[index]) != line[index]:
  61. return False
  62. return True
  63. def get_scsi_drives() -> list[str]:
  64. # pylint: disable=possibly-used-before-assignment, subprocess-run-check
  65. try:
  66. drives = []
  67. result = run(["cdrecord", "-scanbus"], stdout=PIPE)
  68. output = result.stdout
  69. if result.returncode != 0:
  70. raise CustomException("Command 'cdrecord -scanbus' failed.")
  71. for line in output.split(b"\n"):
  72. print(line)
  73. # pylint: disable=possibly-used-before-assignment
  74. if (
  75. match(rb"\s*[0-9](,[0-9])+\s*[0-9]+", line)
  76. and (not bytes_line_ends_with(line, "*"))
  77. and (not bytes_line_ends_with(line, "HOST ADAPTOR"))
  78. ):
  79. matches = search(rb"[0-9](,[0-9])+", line)
  80. if matches is None:
  81. raise CustomException(f"could not parse line: '{line}'")
  82. drives.append(matches.group().decode("ascii"))
  83. if not drives:
  84. raise CustomException("Could not find any SCSI drives")
  85. return drives
  86. except CustomException as error:
  87. app = QApplication([])
  88. QMessageBox.critical(
  89. None,
  90. "Error",
  91. f"Could not get SCSI drives. Reason: {error}",
  92. )
  93. del app
  94. sys.exit(1)
  95. def get_cd_drives() -> list[str]:
  96. if os.name == "nt":
  97. c = wmi.WMI()
  98. cd_drives = []
  99. for cd in c.Win32_CDROMDrive():
  100. cd_drives.append(cd.Drive)
  101. # pylint: disable=possibly-used-before-assignment
  102. else:
  103. raw_cd_drives = get_devices(DRIVER_DEVICE)
  104. cd_drives = []
  105. for cd in raw_cd_drives:
  106. cd_drives.append(str(cd.get_device()))
  107. return cd_drives
  108. # pylint: disable=possibly-used-before-assignment
  109. def eject_drive(drive: str) -> None: # pyright: ignore
  110. if os.name == "nt":
  111. ctypes.windll.WINMM.mciSendStringW(
  112. f"open {drive} type cdaudio alias d_drive", None, 0, None
  113. )
  114. ctypes.windll.WINMM.mciSendStringW(
  115. "set d_drive door open", None, 0, None
  116. )
  117. else:
  118. try:
  119. raw_drives = get_devices(DRIVER_DEVICE)
  120. drive_ejected = False
  121. for cd in raw_drives:
  122. if str(cd.get_device()) == drive:
  123. drive.eject_media() # pyright: ignore
  124. drive_ejected = True
  125. if not drive_ejected:
  126. raise CustomException(f"Drive {drive} not found")
  127. # pylint: disable=possibly-used-before-assignment
  128. except (DriverUnsupportedError, DeviceException, CustomException):
  129. log(f"Eject of CD-ROM drive {drive} failed") # pyright: ignore