Skip to content

ParseFileName raises AttributeError #615

Description

@churchja

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions