From 0f15063260e00cc14035b374a4f1f98712d049d8 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sat, 11 Apr 2026 04:39:33 -0500 Subject: [PATCH] fix(bazarr): patch ffprobe refiner to handle BoxList audio language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio tracks in some video files return language as a BoxList (list of Language objects) from the video metadata parser. The existing code tries to add this directly to a Python set, which fails because BoxList is not hashable — crashing subtitle search for every movie. Fix: check if lang has __iter__ and iterate over it, or call .alpha3 directly if it's a single Language object. Delivered via ConfigMap volumeMount so it survives pod restarts. --- servers/legion/k8s/media/bazarr.yaml | 110 +++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/servers/legion/k8s/media/bazarr.yaml b/servers/legion/k8s/media/bazarr.yaml index 8aca739..d27dc07 100644 --- a/servers/legion/k8s/media/bazarr.yaml +++ b/servers/legion/k8s/media/bazarr.yaml @@ -5,6 +5,107 @@ # 2. Settings > Sonarr: URL=http://sonarr:8989, API key from Sonarr Settings > General # 3. Settings > Providers: add OpenSubtitles.com (free account) for best coverage # 4. Settings > Languages: add English (and any other desired languages) +--- +# Patches Bazarr's ffprobe refiner to handle BoxList (list of Language objects) +# when audio tracks declare multiple languages. Without this, all subtitle searches +# fail with "unhashable type: 'BoxList'" because sets can't contain BoxList objects. +apiVersion: v1 +kind: ConfigMap +metadata: + name: bazarr-patches + namespace: media +data: + ffprobe.py: | + # coding=utf-8 + # fmt: off + + import logging + import json + + from subliminal import Movie + from guessit.jsonutils import GuessitEncoder + + from utilities.path_mappings import path_mappings + from app.database import TableEpisodes, TableMovies, database, select + from utilities.video_analyzer import parse_video_metadata + + + def refine_from_ffprobe(path, video): + if isinstance(video, Movie): + file_id = database.execute( + select(TableMovies.movie_file_id, TableMovies.file_size) + .where(TableMovies.path == path_mappings.path_replace_reverse_movie(path))) \ + .first() + else: + file_id = database.execute( + select(TableEpisodes.episode_file_id, TableEpisodes.file_size) + .where(TableEpisodes.path == path_mappings.path_replace_reverse(path)))\ + .first() + + if not file_id: + return video + + if isinstance(video, Movie): + data = parse_video_metadata(file=path, file_size=file_id.file_size, + movie_file_id=file_id.movie_file_id) + else: + data = parse_video_metadata(file=path, file_size=file_id.file_size, + episode_file_id=file_id.episode_file_id) + + if not data or ('ffprobe' not in data and 'mediainfo' not in data): + logging.debug(f"No cache available for this file: {path}") + return video + + if data['ffprobe']: + logging.debug('FFprobe found: %s', json.dumps(data['ffprobe'], cls=GuessitEncoder, indent=4, + ensure_ascii=False)) + parser_data = data['ffprobe'] + elif data['mediainfo']: + logging.debug('Mediainfo found: %s', json.dumps(data['mediainfo'], cls=GuessitEncoder, indent=4, + ensure_ascii=False)) + parser_data = data['mediainfo'] + else: + parser_data = {} + + if 'video' not in parser_data: + logging.debug('BAZARR parser was unable to find video tracks in the file!') + else: + if 'resolution' in parser_data['video'][0]: + if not video.resolution: + video.resolution = parser_data['video'][0]['resolution'] + if 'codec' in parser_data['video'][0]: + if not video.video_codec: + video.video_codec = parser_data['video'][0]['codec'] + if 'frame_rate' in parser_data['video'][0]: + if not video.fps: + if isinstance(parser_data['video'][0]['frame_rate'], float): + video.fps = parser_data['video'][0]['frame_rate'] + else: + try: + video.fps = parser_data['video'][0]['frame_rate'].magnitude + except AttributeError: + video.fps = parser_data['video'][0]['frame_rate'] + + if 'audio' not in parser_data: + logging.debug('BAZARR parser was unable to find audio tracks in the file!') + else: + if 'codec' in parser_data['audio'][0]: + if not video.audio_codec: + video.audio_codec = parser_data['audio'][0]['codec'] + for track in parser_data['audio']: + if 'language' in track: + lang = track['language'] + # lang may be a single Language object or a BoxList of Language objects + # (BoxList is not hashable and cannot be added to a set directly) + if hasattr(lang, 'alpha3'): + video.audio_languages.add(lang.alpha3) + elif hasattr(lang, '__iter__'): + for l in lang: + if hasattr(l, 'alpha3'): + video.audio_languages.add(l.alpha3) + + return video +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -23,6 +124,9 @@ spec: metadata: labels: app: bazarr + metadata: + annotations: + restartedAt: "2026-04-11T09:40:00Z" spec: containers: - name: bazarr @@ -41,6 +145,9 @@ spec: mountPath: /config - name: media mountPath: /media + - name: bazarr-patches + mountPath: /app/bazarr/bin/bazarr/subtitles/refiners/ffprobe.py + subPath: ffprobe.py resources: requests: memory: 128Mi @@ -68,6 +175,9 @@ spec: - name: media persistentVolumeClaim: claimName: media-data + - name: bazarr-patches + configMap: + name: bazarr-patches --- apiVersion: v1 kind: Service