91bfc42989
Scaled to replicas: 0: fornax (coordinator/ui/workers), jellyfin via media, overseerr, plex, radarr, sonarr, bazarr, prowlarr, flaresolverr, ollama, harmonic-wordpress, docuseal, listmonk, memos, plane, redpanda, wp-coordinator
194 lines
6.7 KiB
YAML
194 lines
6.7 KiB
YAML
# Bazarr — automatic subtitle downloader
|
|
# Connects to Radarr + Sonarr, downloads subtitles from OpenSubtitles/Subscene/etc.
|
|
# After deploy, configure at bazarr:6767:
|
|
# 1. Settings > Radarr: URL=http://radarr:7878, API key from Radarr Settings > General
|
|
# 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:
|
|
name: bazarr
|
|
namespace: media
|
|
labels:
|
|
app: bazarr
|
|
spec:
|
|
replicas: 0
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app: bazarr
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: bazarr
|
|
annotations:
|
|
restartedAt: "2026-04-11T09:40:00Z"
|
|
spec:
|
|
containers:
|
|
- name: bazarr
|
|
image: lscr.io/linuxserver/bazarr:latest
|
|
env:
|
|
- name: PUID
|
|
value: "1000"
|
|
- name: PGID
|
|
value: "1000"
|
|
- name: TZ
|
|
value: "America/Chicago"
|
|
ports:
|
|
- containerPort: 6767
|
|
volumeMounts:
|
|
- name: config
|
|
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
|
|
cpu: 50m
|
|
limits:
|
|
memory: 512Mi
|
|
cpu: 200m
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 6767
|
|
initialDelaySeconds: 120
|
|
periodSeconds: 30
|
|
failureThreshold: 5
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /
|
|
port: 6767
|
|
initialDelaySeconds: 60
|
|
periodSeconds: 15
|
|
volumes:
|
|
- name: config
|
|
persistentVolumeClaim:
|
|
claimName: bazarr-config
|
|
- name: media
|
|
persistentVolumeClaim:
|
|
claimName: media-data
|
|
- name: bazarr-patches
|
|
configMap:
|
|
name: bazarr-patches
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: bazarr
|
|
namespace: media
|
|
spec:
|
|
selector:
|
|
app: bazarr
|
|
ports:
|
|
- name: http
|
|
port: 6767
|
|
targetPort: 6767
|
|
type: ClusterIP
|