devedeng-extended/src/devedeng/avconv.py
Luna 6b624a72cd Narrow bare except blocks so real errors aren't swallowed (roadmap A4)
Replaced all 46 bare 'except:' across the package with specific exception
types, and logged the cases that indicate a real problem:

- config load/save: FileNotFoundError is the silent first-run case; other
  OSError/ValueError/UnicodeDecodeError are logged.
- ffprobe/avprobe: JSON parse failures are now logged (previously a media
  file that ffprobe couldn't read just silently failed to import); decode
  fallbacks -> UnicodeDecodeError; duration parse -> ValueError/TypeError/
  KeyError; install check -> OSError/SubprocessError.
- executor: pre_/post_function and progress-polling hooks now use hasattr
  checks, and a real exception inside a defined hook is logged instead of
  swallowed; decode fallbacks and numeric parses narrowed.
- best-effort filesystem ops (makedirs/unlink) -> OSError.
- all backend check_is_installed probes -> OSError/SubprocessError.
- shutdown: also fixes a latent NameError ('failure' was only set in the
  except branch, so a successful logind PowerOff would crash the handler).

No bare 'except:' remain. Full suite (51 tests) green; GUI starts clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 01:02:33 -07:00

656 lines
27 KiB
Python

#!/usr/bin/env python3
# Copyright 2014 (C) Raster Software Vigo (Sergio Costas)
#
# This file is part of DeVeDe-NG
#
# DeVeDe-NG is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3
# as published by the Free Software Foundation.
#
# DeVeDe-NG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import subprocess
import os
import devedeng.configuration_data
import devedeng.avbase
import devedeng.mux_dvd_menu
import devedeng.project
class avconv(devedeng.avbase.avbase):
supports_analize = False
supports_play = False
supports_convert = True
supports_menu = True
supports_mkiso = False
supports_burn = False
display_name = "AVCONV"
disc_types = []
@staticmethod
def check_is_installed():
try:
handle = subprocess.Popen(
["avconv", "-codecs"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = handle.communicate()
if 0 == handle.wait():
mp2 = False
mp3 = False
ac3 = False
mpeg1 = False
mpeg2 = False
divx = False
h264 = False
hevc = False
for line in stdout.decode("latin-1").split("\n"):
parts = line.strip().split(" ")
if len(parts) < 2:
continue
if len(parts[0]) != 6:
continue
capabilities = parts[0]
codec = parts[1]
if capabilities[1] != 'E':
continue
if (codec == "mpeg1video"):
mpeg1 = True
continue
if (codec == "mpeg2video"):
mpeg2 = True
continue
if (codec == "mp2"):
mp2 = True
continue
if (codec == "mp3"):
mp3 = True
continue
if (codec == "ac3"):
ac3 = True
continue
if (codec == "h264") or (codec == "H264"):
h264 = True
continue
if (codec == "hevc") or (codec == "HEVC"):
hevc = True
continue
if (codec == "mpeg4"):
divx = True
continue
if (mpeg1 and mp2):
devedeng.avconv.avconv.disc_types.append("vcd")
if (mpeg2 and mp2):
devedeng.avconv.avconv.disc_types.append("svcd")
devedeng.avconv.avconv.disc_types.append("cvd")
if (mpeg2 and mp2 and ac3):
devedeng.avconv.avconv.disc_types.append("dvd")
if (divx and mp3):
devedeng.avconv.avconv.disc_types.append("divx")
if (h264 and mp3):
devedeng.avconv.avconv.disc_types.append("mkv")
if (hevc and mp3):
devedeng.ffmpeg.ffmpeg.disc_types.append("hevc")
return True
else:
return False
except (OSError, subprocess.SubprocessError):
return False
def __init__(self):
devedeng.executor.executor.__init__(self)
self.config = devedeng.configuration_data.configuration.get_config()
self.check_version(["avconv", "-version"])
def convert_file(self, file_project, output_file, video_length, pass2=False, start_offset=0):
self.start_offset = start_offset
if file_project.two_pass_encoding:
if pass2:
self.text = _("Converting %(X)s (pass 2)") % {
"X": file_project.title_name}
else:
self.text = _("Converting %(X)s (pass 1)") % {
"X": file_project.title_name}
# Prepare the converting process for the second pass
tmp = devedeng.avconv.avconv()
tmp.convert_file(file_project, output_file, video_length, True,
start_offset)
# it deppends of this process
tmp.add_dependency(self)
# add it as a child process of this one
self.add_child_process(tmp)
else:
self.text = _("Converting %(X)s") % {"X": file_project.title_name}
if (pass2 == False) and (file_project.two_pass_encoding == True):
# this is the first pass in a 2-pass codification
second_pass = False
else:
# second_pass is TRUE in the second pass of a 2-pass codification, and also when not doing 2-pass codification
# It is used to remove unnecessary steps during the first pass, but
# that are needed on the second pass, or when not using 2-pass
# codification
second_pass = True
if (video_length == 0):
self.final_length = file_project.original_length
else:
self.final_length = video_length
self.command_var = []
# lower priority so the desktop stays responsive during the transcode
self.command_var.append("nice")
self.command_var.append("-n")
self.command_var.append("10")
self.command_var.append("avconv")
if start_offset and start_offset > 0:
self.command_var.append("-ss")
self.command_var.append(str(start_offset))
self.command_var.append("-i")
self.command_var.append(file_project.file_name)
if (file_project.volume != 100) and second_pass:
self.command_var.append("-vol")
self.command_var.append(
str(int((256 * file_project.volume) / 100)))
if (file_project.audio_delay != 0.0) and (file_project.copy_sound == False) and (file_project.no_reencode_audio_video == False) and second_pass:
self.command_var.append("-itsoffset")
self.command_var.append(str(file_project.audio_delay))
if start_offset and start_offset > 0:
self.command_var.append("-ss")
self.command_var.append(str(start_offset))
self.command_var.append("-i")
self.command_var.append(file_project.file_name)
self.command_var.append("-map")
self.command_var.append("1:" + str(file_project.video_list[0]))
if (not file_project.copy_sound) and (not file_project.no_reencode_audio_video):
for l in file_project.audio_list:
self.command_var.append("-map")
self.command_var.append("0:" + str(l))
if (file_project.no_reencode_audio_video == False) and second_pass:
cmd_line = ""
if file_project.deinterlace == "deinterlace_yadif":
cmd_line += "yadif"
vflip = False
hflip = False
if (file_project.prerotation == 90):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "transpose=1"
elif (file_project.prerotation == -90) or (file_project.prerotation == 270):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "transpose=2"
elif (file_project.prerotation == 180):
if (cmd_line != ""):
cmd_line += ",fifo,"
vflip = not vflip
hflip = not hflip
if (file_project.rotation == "rotation_90"):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "transpose=1"
elif (file_project.rotation == "rotation_270"):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "transpose=2"
elif (file_project.rotation == "rotation_180"):
vflip = not vflip
hflip = not hflip
if (file_project.mirror_vertical):
vflip = not vflip
if (file_project.mirror_horizontal):
hflip = not hflip
if (vflip):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "vflip"
if (hflip):
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += "hflip"
if (file_project.width_midle != file_project.original_width) or (file_project.height_midle != file_project.original_height):
if (cmd_line != ""):
cmd_line += ",fifo,"
# MPEG-2 / yuv420p needs even dimensions; round to even so the
# aspect math can't produce an odd pad/crop target (e.g. 1079).
mid_w = file_project.width_midle - (file_project.width_midle % 2)
mid_h = file_project.height_midle - (file_project.height_midle % 2)
x = int((mid_w - file_project.original_width) / 2)
y = int((mid_h - file_project.original_height) / 2)
if (x > 0) or (y > 0):
cmd_line += "pad=" + str(mid_w) + ":" + str(
mid_h) + ":" + str(x) + ":" + str(y) + ":0x000000"
else:
# crop x/y are the (positive) top-left offset into the source
cmd_line += "crop=" + str(mid_w) + ":" + str(
mid_h) + ":" + str(abs(x)) + ":" + str(abs(y))
if (file_project.width_final != file_project.width_midle) or (file_project.height_final != file_project.height_midle):
if (cmd_line != ""):
cmd_line += ",fifo,"
if self.major_version < 11:
cmd_line += "scale=" + \
str(file_project.width_final) + ":" + \
str(file_project.height_final)
else:
cmd_line += "scale=w=" + \
str(file_project.width_final) + ":h=" + \
str(file_project.height_final)
# text subtitle burn-in (hardsub) as the last filter, after scaling.
# Image-based subs need an overlay filtergraph and are only handled
# by the ffmpeg backend; avconv users get text subs burned in.
if second_pass:
text_filter = self._avconv_text_subtitle_filter(file_project)
if text_filter is not None:
if (cmd_line != ""):
cmd_line += ",fifo,"
cmd_line += text_filter
if cmd_line != "":
self.command_var.append("-vf")
self.command_var.append(cmd_line)
self.command_var.append("-y")
vcd = False
maxrate = int(file_project.video_rate_final * 1500)
minrate = int(file_project.video_rate_final * 666.66)
if (self.config.disc_type == "divx"):
self.command_var.append("-vcodec")
self.command_var.append("mpeg4")
self.command_var.append("-acodec")
self.command_var.append("libmp3lame")
self.command_var.append("-f")
self.command_var.append("avi")
if not pass2:
self.command_var.append("-maxrate:v")
self.command_var.append(str(maxrate))
self.command_var.append("-minrate:v")
if minrate < (devedeng.project.devede_project.divx_min_bps * 1000):
minrate = devedeng.project.devede_project.divx_min_bps * 1000
self.command_var.append(str(minrate))
elif (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc"):
self.command_var.append("-vcodec")
self.command_var.append("h264" if self.config.disc_type == "mkv" else "hevc")
self.command_var.append("-acodec")
self.command_var.append("libmp3lame")
self.command_var.append("-f")
self.command_var.append("matroska")
if not pass2:
self.command_var.append("-maxrate:v")
self.command_var.append(str(maxrate))
self.command_var.append("-minrate:v")
if minrate < (devedeng.project.devede_project.mkv_min_bps * 1000):
minrate = devedeng.project.devede_project.mkv_min_bps * 1000
self.command_var.append(str(minrate))
elif (self.config.disc_type == "dvd"):
if not file_project.no_reencode_audio_video:
self.command_var.append("-c:v")
self.command_var.append("mpeg2video")
if not file_project.copy_sound:
if file_project.sound5_1:
self.command_var.append("-c:a")
self.command_var.append("ac3")
else:
self.command_var.append("-c:a")
if file_project.format_pal:
self.command_var.append("ac3")
else:
self.command_var.append("mp2")
self.command_var.append("-f")
self.command_var.append("dvd")
self.command_var.append("-r")
if file_project.format_pal:
self.command_var.append("25")
else:
if (file_project.original_fps == 24):
self.command_var.append("24000/1001")
else:
self.command_var.append("30000/1001")
self.command_var.append("-pix_fmt")
self.command_var.append("yuv420p")
self.command_var.append("-maxrate:v")
if maxrate > (devedeng.project.devede_project.dvd_max_bps_base[0] * 1000):
maxrate = devedeng.project.devede_project.dvd_max_bps_base[0] * 1000
self.command_var.append(str(maxrate))
if not pass2:
self.command_var.append("-minrate:v")
if minrate < (devedeng.project.devede_project.dvd_min_bps * 1000):
minrate = devedeng.project.devede_project.dvd_min_bps * 1000
self.command_var.append(str(minrate))
self.command_var.append("-bufsize")
self.command_var.append("1835008")
self.command_var.append("-packetsize")
self.command_var.append("2048")
self.command_var.append("-muxrate")
self.command_var.append("10080000")
self.command_var.append("-ar")
self.command_var.append("48000")
elif (self.config.disc_type == "vcd"):
vcd = True
if not file_project.no_reencode_audio_video:
self.command_var.append("-c:v")
self.command_var.append("mpeg1video")
if not file_project.copy_sound:
self.command_var.append("-c:a")
self.command_var.append("mp2")
self.command_var.append("-b:v")
self.command_var.append("1150000")
self.command_var.append("-b:a")
self.command_var.append("224000")
self.command_var.append("-f")
self.command_var.append("vcd")
self.command_var.append("-r")
if file_project.format_pal:
self.command_var.append("25")
else:
if (file_project.original_fps == 24):
self.command_var.append("24000/1001")
else:
self.command_var.append("30000/1001")
self.command_var.append("-g")
if file_project.format_pal:
self.command_var.append("15")
else:
self.command_var.append("18")
self.command_var.append("-s")
if file_project.format_pal:
self.command_var.append("352x288")
else:
self.command_var.append("352x240")
self.command_var.append("-maxrate:v")
self.command_var.append("1150000")
self.command_var.append("-minrate:v")
self.command_var.append("1150000")
self.command_var.append("-bufsize")
self.command_var.append("327680")
self.command_var.append("-packetsize")
self.command_var.append("2324")
self.command_var.append("-muxrate")
self.command_var.append("1411200")
self.command_var.append("-ar")
self.command_var.append("44100")
elif (self.config.disc_type == "svcd") or (self.config.disc_type == "cvd"):
if not file_project.no_reencode_audio_video:
self.command_var.append("-c:v")
self.command_var.append("mpeg2video")
if not file_project.copy_sound:
self.command_var.append("-c:a")
self.command_var.append("mp2")
self.command_var.append("-f")
self.command_var.append("svcd")
self.command_var.append("-r")
if file_project.format_pal:
self.command_var.append("25")
else:
if (file_project.original_fps == 24):
self.command_var.append("24000/1001")
else:
self.command_var.append("30000/1001")
self.command_var.append("-g")
if file_project.format_pal:
self.command_var.append("12")
else:
self.command_var.append("15")
self.command_var.append("-s")
if self.config.disc_type == "cvd":
if file_project.format_pal:
self.command_var.append("352x576")
else:
self.command_var.append("352x480")
else:
if file_project.format_pal:
self.command_var.append("480x576")
else:
self.command_var.append("480x480")
self.command_var.append("-pix_fmt")
self.command_var.append("yuv420p")
self.command_var.append("-maxrate:v")
if maxrate > (devedeng.project.devede_project.svcd_max_bps_base[0] * 1000):
maxrate = devedeng.project.devede_project.svcd_max_bps_base[0] * 1000
self.command_var.append(str(maxrate))
if not pass2:
self.command_var.append("-minrate:v")
if minrate < (devedeng.project.devede_project.svcd_min_bps * 1000):
minrate = (devedeng.project.devede_project.svcd_min_bps * 1000)
self.command_var.append(str(minrate))
self.command_var.append("-bufsize")
self.command_var.append("1835008")
self.command_var.append("-packetsize")
self.command_var.append("2324")
self.command_var.append("-ar")
self.command_var.append("44100")
self.command_var.append("-scan_offset")
self.command_var.append("1")
if (not file_project.no_reencode_audio_video):
self.command_var.append("-sn") # no subtitles
if file_project.copy_sound or file_project.no_reencode_audio_video:
self.command_var.append("-acodec")
self.command_var.append("copy")
if file_project.no_reencode_audio_video:
self.command_var.append("-vcodec")
self.command_var.append("copy")
if (not vcd):
if not file_project.format_pal:
if (file_project.original_fps == 24) and ((self.config.disc_type == "dvd")):
keyintv = 15
else:
keyintv = 18
else:
keyintv = 15
if (not file_project.gop12) and (not(self.config.disc_type == "divx")) and (not(self.config.disc_type == "mkv") and (not(self.config.disc_type == "hevc"))):
self.command_var.append("-g")
self.command_var.append(str(keyintv))
if (self.config.disc_type == "divx") or (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc"):
self.command_var.append("-g")
self.command_var.append("300")
elif file_project.gop12 and (not file_project.no_reencode_audio_video):
self.command_var.append("-g")
self.command_var.append("12")
self.command_var.append("-bf")
self.command_var.append("2")
self.command_var.append("-strict")
self.command_var.append("1")
if video_length != 0:
self.command_var.append("-t")
self.command_var.append(str(video_length))
self.command_var.append("-ac")
if (file_project.sound5_1) and ((self.config.disc_type == "dvd") or (self.config.disc_type == "divx") or (self.config.disc_type == "mkv") or (self.config.disc_type == "hevc")):
self.command_var.append("6")
else:
self.command_var.append("2")
self.command_var.append("-aspect")
self.command_var.append(str(file_project.aspect_ratio_final))
if self.config.disc_type == "divx":
self.command_var.append("-vtag")
self.command_var.append("DX50")
if (file_project.deinterlace == "deinterlace_ffmpeg") and (file_project.no_reencode_audio_video == False) and second_pass:
self.command_var.append("-deinterlace")
if (file_project.no_reencode_audio_video == False) and (vcd == False) and second_pass:
self.command_var.append("-s")
self.command_var.append(
str(file_project.width_final) + "x" + str(file_project.height_final))
if second_pass:
self.command_var.append("-trellis")
self.command_var.append("1")
self.command_var.append("-mbd")
self.command_var.append("2")
else:
self.command_var.append("-trellis")
self.command_var.append("0")
self.command_var.append("-mbd")
self.command_var.append("0")
if (vcd == False) and (file_project.no_reencode_audio_video == False):
self.command_var.append("-b:a")
self.command_var.append(str(self._adjust_audio_bitrate(file_project.audio_rate_final)*1000))
self.command_var.append("-b:v")
self.command_var.append(str(file_project.video_rate_final*1000))
if file_project.two_pass_encoding == True:
self.command_var.append("-passlogfile")
self.command_var.append(output_file + ".passlog")
self.command_var.append("-pass")
if pass2:
self.command_var.append("2")
else:
self.command_var.append("1")
self.command_var.append(output_file)
def _adjust_audio_bitrate(self, bitrate):
# includes AC3 rates above the MP2 ceiling (384) so 448k AC3 isn't
# silently clamped down
br = [32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
448, 512, 640]
for a in br:
if a >= bitrate:
return a
return br[-1]
def _avconv_text_subtitle_filter(self, file_project):
""" Build a `subtitles=` filter to burn a text-based embedded subtitle
track into the picture, or None. Image-based subs are not handled by
the avconv backend (use the ffmpeg backend for those). """
if getattr(file_project, "hardsub_kind", "text") != "text":
return None
index = getattr(file_project, "hardsub_index", -1)
if index is None or index < 0:
return None
subs = getattr(file_project, "embedded_subtitles", []) or []
rel = None
for i, track in enumerate(subs):
if track["index"] == index:
rel = i
break
if rel is None:
return None
path = file_project.file_name.replace("\\", "\\\\").replace(":", "\\:")
path = path.replace("'", "\\'")
return "subtitles='%s':si=%d" % (path, rel)
def create_menu_mpeg(self, n_page, background_music, sound_length, pal, video_rate, audio_rate, output_path, use_mp2, widescreen):
self.n_page = n_page
self.final_length = float(sound_length)
self.text = _("Creating menu %(X)d") % {"X": self.n_page}
self.command_var = []
self.command_var.append("avconv")
self.command_var.append("-loop")
self.command_var.append("1")
self.command_var.append("-f")
self.command_var.append("image2")
self.command_var.append("-i")
self.command_var.append(os.path.join(
output_path, "menu_" + str(n_page) + "_bg.png"))
self.command_var.append("-i")
self.command_var.append(background_music)
self.command_var.append("-y")
self.command_var.append("-target")
if pal:
self.command_var.append("pal-dvd")
else:
self.command_var.append("ntsc-dvd")
self.command_var.append("-acodec")
if use_mp2:
self.command_var.append("mp2")
if (audio_rate > 384):
audio_rate = 384 # max bitrate for mp2
else:
self.command_var.append("ac3")
self.command_var.append("-s")
if pal:
self.command_var.append("720x576")
else:
self.command_var.append("720x480")
self.command_var.append("-g")
self.command_var.append("12")
self.command_var.append("-b:v")
self.command_var.append(str(video_rate) + "k")
self.command_var.append("-b:a")
self.command_var.append(str(self._adjust_audio_bitrate(audio_rate)) + "k")
self.command_var.append("-aspect")
if widescreen:
self.command_var.append("16:9")
else:
self.command_var.append("4:3")
self.command_var.append("-t")
self.command_var.append(str(1 + sound_length))
movie_path = os.path.join(output_path, "menu_" + str(n_page) + ".mpg")
self.command_var.append(movie_path)
muxer = devedeng.mux_dvd_menu.mux_dvd_menu()
final_path = muxer.create_mpg(n_page, output_path, movie_path)
# the muxer process depends of the converter process
muxer.add_dependency(self)
self.add_child_process(muxer)
return (final_path)
def process_stdout(self, data):
return
def process_stderr(self, data):
pos = data[0].find("time=")
if (pos != -1):
pos += 5
pos2 = data[0].find(" ", pos)
if (pos2 != -1):
parts = data[0][pos:pos2].split(":")
t = 0.0
for e in parts:
try:
v = float(e)
except ValueError:
continue
t = t * 60.0 + v
t /= self.final_length
self.progress_bar[1].set_fraction(t)
self.progress_bar[1].set_text("%.1f%%" % (100.0 * t))