Files
Classroom_Desktop/lib/classroom_optical.py
T
ekklesiaandClaude Opus 5 0c1acfe5f5 feat: detect and play DVDs where a drive is fitted
DVD support stays optional per room: DVD_ENABLED decides whether the
button is built at all, and rooms without it poll nothing. Upgrades
append a commented DVD_ENABLED=false so the option is discoverable
without changing what those rooms already do.

The dock polls the drive with CDROM_DRIVE_STATUS, which is a status
query rather than a read, so it does not spin the disc up. A disc that
becomes ready announces itself once and brings the DVD logo up to full
strength; an empty or unplugged drive holds it back, the same gray
versus colour language the LIVE icon already uses. Nothing autoplays —
playback always needs a deliberate press.

Playback is VLC rather than mpv because mpv dropped DVD menu support,
and menus are the part that matters for a commercial disc. It goes
through classroom-window like everything else, so the player minimizes
and restores from the dock exactly as the browser does.

Pressing the button with no drive, no disc, or a disc still spinning up
gives a plain-language notification instead of failing silently.

Reading the drive needs group cdrom, which the postinst grants and the
OTA reboot activates. Commercial CSS discs additionally need libdvdcss,
which Debian cannot ship; classroom-dvd-css-setup bootstraps it from
contrib. That is deliberately not run from the postinst, which executes
while dpkg already holds the apt lock.

The optical helpers live in a shared module both the dock and
classroom-dvd import, and the dock imports it defensively: if it ever
goes missing the shell loses disc detection rather than failing to
start.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-04 17:07:25 -05:00

126 lines
2.8 KiB
Python

"""Optical drive helpers for the Ekklesia Classroom shell.
Everything here is deliberately dependency free and non-blocking so the
dock can poll drive state from its GTK main loop without stalling and
without spinning the drive up.
"""
import fcntl
import glob
import os
# linux/cdrom.h
CDROM_DRIVE_STATUS = 0x5326
CDROM_DISC_STATUS = 0x5327
CDROMEJECT = 0x5309
CDROMCLOSETRAY = 0x5319
# CDSL_CURRENT: "whatever slot is loaded right now".
CDSL_CURRENT = 0x7FFFFFFF
# Drive status values.
CDS_NO_INFO = 0
CDS_NO_DISC = 1
CDS_TRAY_OPEN = 2
CDS_DRIVE_NOT_READY = 3
CDS_DISC_OK = 4
# Disc status values.
CDS_AUDIO = 100
CDS_DATA_1 = 101
CDS_DATA_2 = 102
CDS_XA_2_1 = 103
CDS_XA_2_2 = 104
CDS_MIXED = 105
def devices():
"""Every optical device node currently present."""
return sorted(glob.glob("/dev/sr[0-9]*"))
def default_device():
"""The optical device the classroom should use, if any."""
found = devices()
if not found:
return None
return found[0]
def _ioctl(device, request, argument=CDSL_CURRENT):
"""Run a CD-ROM ioctl, returning None when the drive cannot be read."""
try:
fd = os.open(device, os.O_RDONLY | os.O_NONBLOCK)
except OSError:
return None
try:
return fcntl.ioctl(fd, request, argument)
except OSError:
return None
finally:
os.close(fd)
def drive_status(device=None):
"""CDS_* drive status, or None when there is no readable drive."""
if device is None:
device = default_device()
if device is None:
return None
return _ioctl(device, CDROM_DRIVE_STATUS)
def disc_status(device=None):
"""CDS_* disc type. Only call this once a disc has become ready."""
if device is None:
device = default_device()
if device is None:
return None
return _ioctl(device, CDROM_DISC_STATUS)
def has_disc(device=None):
"""True when a disc is loaded and the drive has finished reading it."""
return drive_status(device) == CDS_DISC_OK
def eject(device=None):
"""Open the tray. Returns True when the drive accepted the request."""
if device is None:
device = default_device()
if device is None:
return False
return _ioctl(device, CDROMEJECT, 0) is not None
def close_tray(device=None):
"""Close the tray. Returns True when the drive accepted the request."""
if device is None:
device = default_device()
if device is None:
return False
return _ioctl(device, CDROMCLOSETRAY, 0) is not None
def describe(status):
"""Human readable drive status, used by classroom-dvd status."""
return {
None: "no drive",
CDS_NO_INFO: "unknown",
CDS_NO_DISC: "no disc",
CDS_TRAY_OPEN: "tray open",
CDS_DRIVE_NOT_READY: "drive not ready",
CDS_DISC_OK: "disc ready",
}.get(status, "unknown")