ParseFileName raises AttributeError on filenames that don't match the expected YYYY-MM-DD...HH:MM:SS pattern
Summary
ParseFileName.__init__ in scripts/utils/classes.py calls .group() on the result of re.search() without checking for None. Any file in StreamData whose name doesn't match the expected YYYY-MM-DD-...-HH:MM:SS.wav pattern crashes the analyzer with a misleading AttributeError: 'NoneType' object has no attribute 'group' traceback. The analyzer recovers (the try/except in process_file catches it) but the crash and traceback log entry happen for every offending file.
Environment
- Fork:
Nachtzuster/BirdNET-Pi, branch main, commit 88985a3
- Hardware: Raspberry Pi 5
- OS: Debian Trixie
Reproduction
Drop any file with a non-conforming name into $RECS_DIR/StreamData (e.g. touch ~/BirdSongs/StreamData/foo.wav and write some bytes — sox -n -r 48000 -c 1 foo.wav synth 1 sine 440 works). The inotify watch fires on IN_CLOSE_WRITE, process_file is invoked, and the analyzer logs:
[birdnet_analysis][INFO] Analyzing /home/birder/BirdSongs/StreamData/foo.wav
[birdnet_analysis][ERROR] Unexpected error:
Traceback (most recent call last):
File "/usr/local/bin/birdnet_analysis.py", line 91, in process_file
file = ParseFileName(file_name)
File "/home/birder/BirdNET-Pi/scripts/utils/classes.py", line 33, in __init__
date_created = re.search('^[0-9]+-[0-9]+-[0-9]+', name).group()
AttributeError: 'NoneType' object has no attribute 'group'
In my case the offending file was spectrogram_window.tmp.wav from spectrogram.sh (filed separately as bug #1), but the underlying defensive-coding gap is independent of that source — any unexpected file lands the analyzer in this state.
Root cause
scripts/utils/classes.py, lines 30–34:
class ParseFileName:
def __init__(self, file_name):
self.file_name = file_name
name = os.path.splitext(os.path.basename(file_name))[0]
date_created = re.search('^[0-9]+-[0-9]+-[0-9]+', name).group()
time_created = re.search('[0-9]+:[0-9]+:[0-9]+$', name).group()
Both re.search calls return None when the pattern doesn't match. .group() on None is the AttributeError we see.
Proposed fix
Validate both regex matches and raise a clear, typed exception that callers can catch and treat as "skip this file":
+class ParseFileNameError(ValueError):
+ """Raised when a file's name doesn't match the expected pattern."""
+
class ParseFileName:
def __init__(self, file_name):
self.file_name = file_name
name = os.path.splitext(os.path.basename(file_name))[0]
- date_created = re.search('^[0-9]+-[0-9]+-[0-9]+', name).group()
- time_created = re.search('[0-9]+:[0-9]+:[0-9]+$', name).group()
+ date_match = re.search('^[0-9]+-[0-9]+-[0-9]+', name)
+ time_match = re.search('[0-9]+:[0-9]+:[0-9]+$', name)
+ if date_match is None or time_match is None:
+ raise ParseFileNameError(
+ f"Filename {name!r} does not match expected "
+ f"YYYY-MM-DD-...-HH:MM:SS pattern"
+ )
+ date_created = date_match.group()
+ time_created = time_match.group()
self.file_date = datetime.datetime.strptime(
f'{date_created}T{time_created}', "%Y-%m-%dT%H:%M:%S")
Then in birdnet_analysis.py, process_file can handle this case explicitly without firing the catch-all BaseException path:
def process_file(file_name, report_queue):
try:
if os.path.getsize(file_name) == 0:
os.remove(file_name)
return
log.info('Analyzing %s', file_name)
with open(ANALYZING_NOW, 'w') as analyzing:
analyzing.write(file_name)
- file = ParseFileName(file_name)
+ try:
+ file = ParseFileName(file_name)
+ except ParseFileNameError as e:
+ log.warning('Skipping unexpected file in StreamData: %s', e)
+ return
detections = run_analysis(file)
This turns a 9-line traceback into a single WARNING log line, which is the right level for "I noticed something unexpected and skipped it."
Why bother fixing this if bug #1 is the root cause
Bug #1 (the spectrogram.sh temp file) is the source of the problem I personally observed, but StreamData is a directory other tools could also touch — RTSP capture, manual file drops for testing, partial writes from a crashed arecord, etc. The analyzer should fail safely on any unexpected filename rather than spew tracebacks. This is a defensive-coding improvement that's worth doing on its own merits, independent of #1.
I'd be happy to open a PR with this fix if useful.
ParseFileNameraisesAttributeErroron filenames that don't match the expectedYYYY-MM-DD...HH:MM:SSpatternSummary
ParseFileName.__init__inscripts/utils/classes.pycalls.group()on the result ofre.search()without checking forNone. Any file inStreamDatawhose name doesn't match the expectedYYYY-MM-DD-...-HH:MM:SS.wavpattern crashes the analyzer with a misleadingAttributeError: 'NoneType' object has no attribute 'group'traceback. The analyzer recovers (thetry/exceptinprocess_filecatches it) but the crash and traceback log entry happen for every offending file.Environment
Nachtzuster/BirdNET-Pi, branchmain, commit88985a3Reproduction
Drop any file with a non-conforming name into
$RECS_DIR/StreamData(e.g.touch ~/BirdSongs/StreamData/foo.wavand write some bytes —sox -n -r 48000 -c 1 foo.wav synth 1 sine 440works). Theinotifywatch fires onIN_CLOSE_WRITE,process_fileis invoked, and the analyzer logs:In my case the offending file was
spectrogram_window.tmp.wavfromspectrogram.sh(filed separately as bug #1), but the underlying defensive-coding gap is independent of that source — any unexpected file lands the analyzer in this state.Root cause
scripts/utils/classes.py, lines 30–34:Both
re.searchcalls returnNonewhen the pattern doesn't match..group()onNoneis theAttributeErrorwe see.Proposed fix
Validate both regex matches and raise a clear, typed exception that callers can catch and treat as "skip this file":
Then in
birdnet_analysis.py,process_filecan handle this case explicitly without firing the catch-allBaseExceptionpath:def process_file(file_name, report_queue): try: if os.path.getsize(file_name) == 0: os.remove(file_name) return log.info('Analyzing %s', file_name) with open(ANALYZING_NOW, 'w') as analyzing: analyzing.write(file_name) - file = ParseFileName(file_name) + try: + file = ParseFileName(file_name) + except ParseFileNameError as e: + log.warning('Skipping unexpected file in StreamData: %s', e) + return detections = run_analysis(file)This turns a 9-line traceback into a single
WARNINGlog line, which is the right level for "I noticed something unexpected and skipped it."Why bother fixing this if bug #1 is the root cause
Bug #1 (the
spectrogram.shtemp file) is the source of the problem I personally observed, butStreamDatais a directory other tools could also touch — RTSP capture, manual file drops for testing, partial writes from a crashedarecord, etc. The analyzer should fail safely on any unexpected filename rather than spew tracebacks. This is a defensive-coding improvement that's worth doing on its own merits, independent of #1.I'd be happy to open a PR with this fix if useful.