# 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
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
import os
import time
import shutil
import urllib.parse
import pickle
import math
import devedeng.file_movie
import devedeng.ask
import devedeng.add_files
import devedeng.message
import devedeng.dvd_menu
import devedeng.create_disk_window
import devedeng.runner
import devedeng.settings
import devedeng.dvdauthor_converter
import devedeng.mkisofs
import devedeng.end_job
import devedeng.vcdimager_converter
import devedeng.shutdown
import devedeng.about
import devedeng.opensave
import devedeng.help
import devedeng.separator
def compute_high_profile_bitrate(total_runtime_s, total_audio_kbits,
disc_capacity_kbytes, menu_kbytes=0.0,
cap=9000, floor=3000):
""" Pure helper for the High-quality profile (no GTK), so it can be tested.
Strategy ("fewest discs, then fill"): use the fewest discs that can
hold the content without dropping below the quality floor (densest
packing), then raise the video bitrate to exactly fill those discs
(clamped to [floor, cap]). A short movie ends up on one disc near the
cap; a long one spreads across the minimum number of discs and fills
each, rather than spilling onto extra discs at maximum bitrate.
Returns (n_discs, video_bitrate_kbps).
"""
if total_runtime_s <= 0:
return (1, cap)
disc_kbits = disc_capacity_kbytes * 8.0
menu_kbits = menu_kbytes * 8.0
audio_kbits = total_audio_kbits
# how big is the content at the LOWEST acceptable quality? that gives the
# densest packing, i.e. the fewest discs needed
content_at_floor_kbits = (floor * total_runtime_s) + audio_kbits + menu_kbits
n = max(1, int(math.ceil(content_at_floor_kbits / disc_kbits)))
# fill those n discs: spread the available video budget over the runtime
video_budget_kbits = (n * disc_kbits) - audio_kbits - menu_kbits
video_bitrate = video_budget_kbits / total_runtime_s
if video_bitrate > cap:
video_bitrate = cap
if video_bitrate < floor:
video_bitrate = floor
return (n, int(video_bitrate))
def _movie_chapter_seconds(movie):
""" Returns a sorted list of candidate chapter-cut times (seconds) for a
movie: the automatic chapters (every chapter_size minutes) merged with
any manual chapter entries. Empty if the movie has no chapters. """
times = []
if not getattr(movie, "divide_in_chapters", False):
return times
length = movie.original_length
step = max(1, int(getattr(movie, "chapter_size", 5))) * 60
t = step
while t < (length - 4):
times.append(t)
t += step
entry = getattr(movie, "chapter_list_entry", None)
if entry:
for chunk in entry.split(","):
chunk = chunk.strip()
if ":" not in chunk:
continue
try:
mm, ss = chunk.split(":")[:2]
secs = int(mm) * 60 + int(ss)
except ValueError:
continue
if 0 < secs < length:
times.append(secs)
return sorted(set(times))
def plan_disc_split(movies, disc_capacity_kbytes, first_disc_reserved_kbytes=0.0):
""" Pure planning logic (no GTK) so it can be unit-tested.
movies: objects exposing get_kbytes_per_second(), original_length, and
(optionally) chapter info via _movie_chapter_seconds().
disc_capacity_kbytes: usable size of one disc in kBytes.
first_disc_reserved_kbytes: space taken by the menu on disc 1.
Returns a list of discs; each disc is a list of segments
(movie, start_sec, duration_sec) # duration 0 => to end of movie
"""
discs = []
current = []
current_used = first_disc_reserved_kbytes
def disc_capacity_for(index):
cap = disc_capacity_kbytes
if index == 0:
cap -= first_disc_reserved_kbytes
return cap
def flush():
nonlocal current, current_used
if current:
discs.append(current)
current = []
current_used = 0.0
for movie in movies:
rate = movie.get_kbytes_per_second() # kBytes per second
if rate <= 0:
rate = 1.0
length = movie.original_length
whole_size = rate * length
remaining_cap = disc_capacity_kbytes - current_used
if whole_size <= remaining_cap:
# fits whole on the current disc
current.append((movie, 0, 0))
current_used += whole_size
continue
# doesn't fit on what's left of the current disc
if whole_size <= disc_capacity_kbytes:
# fits on a fresh disc by itself: start a new disc
flush()
current.append((movie, 0, 0))
current_used = whole_size
continue
# the movie alone is bigger than a whole disc: split it by time.
flush()
chapters = _movie_chapter_seconds(movie)
start = 0
while start < length:
cap = disc_capacity_kbytes # each split-segment gets its own disc
max_secs = cap / rate
target_end = start + max_secs
if target_end >= length:
cut = length
else:
cut = _nearest_chapter_before(chapters, start, target_end)
if cut is None or cut <= start:
cut = target_end # no usable chapter: even time cut
duration = cut - start
# last segment to end of movie is encoded with duration 0
seg_duration = 0 if cut >= length else duration
discs.append([(movie, start, seg_duration)])
start = cut
flush()
if not discs:
discs = [[]]
return discs
def _nearest_chapter_before(chapters, start, target_end):
""" The largest chapter time that is > start and <= target_end, or None. """
best = None
for c in chapters:
if c > start and c <= target_end:
if best is None or c > best:
best = c
return best
class devede_project:
dvd_max_bps_base = {0: 9000}
dvd_min_bps = 300
dvd_max_total = 10000
svcd_max_bps_base = {0: 2700}
svcd_min_bps = 200
svcd_max_total = 2700
divx_max_bps_base = {0: 4854, 720: 9708, 1080: 20000}
divx_min_bps = 300
divx_max_total = -1
mkv_max_bps_base = {0: 192, 240: 2000,
480: 14000, 720: 50000, 1080: 240000}
mkv_min_bps = 192
mkv_max_total = -1
def_max_bps_base = {0: 9000}
def_min_bps = 300
def_max_total = -1
def __init__(self):
self.config = devedeng.configuration_data.configuration.get_config()
self.disc_type = self.config.disc_type
self.menu = devedeng.dvd_menu.dvd_menu()
self.current_title = None
self.project_file = None
builder = Gtk.Builder()
builder.set_translation_domain(self.config.gettext_domain)
builder.add_from_file(os.path.join(self.config.glade, "wmain.ui"))
builder.connect_signals(self)
# Interface widgets
self.wmain_window = builder.get_object("wmain_window")
self.wdisc_size = builder.get_object("disc_size")
self.wliststore_files = builder.get_object("liststore_files")
self.wfiles = builder.get_object("files")
self.wdisc_fill_level = builder.get_object("disc_fill_level")
self.wuse_pal = builder.get_object("use_pal")
self.wuse_ntsc = builder.get_object("use_ntsc")
self.wframe_titles = builder.get_object("frame_titles")
self.winmenu = builder.get_object("treeviewcolumn3")
self.wframe_menu = builder.get_object("frame_menu")
self.wcreate_menu = builder.get_object("create_menu")
self.wmenu_options = builder.get_object("menu_options")
self.wadd_file = builder.get_object("add_file")
self.wdelete_file = builder.get_object("delete_file")
self.wup_file = builder.get_object("up_file")
self.wdown_file = builder.get_object("down_file")
self.wproperties_file = builder.get_object("properties_file")
self.wpreview_file = builder.get_object("preview_file")
self.wcreate_disc = builder.get_object("create_disc")
# DVD encode-profile selector (built in code, packed next to disc size)
self._build_profile_selector()
selection = self.wfiles.get_selection()
selection.set_mode(Gtk.SelectionMode.BROWSE)
if (self.config.PAL):
self.wuse_pal.set_active(True)
self.pal = True
else:
self.wuse_ntsc.set_active(True)
self.pal = False
self.wcreate_menu.set_active(True)
self.config.connect('disc_type', self.set_type)
if (self.disc_type is not None):
self.set_type(None, self.disc_type)
self.wmain_window.drag_dest_set(
Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
targets = Gtk.TargetList.new([])
targets.add(Gdk.Atom.intern("text/uri-list", False), 0, 0)
self.wmain_window.drag_dest_set_target_list(targets)
def set_type(self, obj=None, disc_type=None):
""" this method sets the disk type to the specified one and adapts the interface
in the main window to it. Also leaves everything ready to start creating a
new disc """
if (disc_type is not None):
self.disc_type = disc_type
self.wmain_window.show_all()
self.set_interface_status(None)
# Set the default disc size
if (self.disc_type == "dvd") or (self.disc_type == "mkv") or (self.disc_type == "hevc"):
self.wdisc_size.set_active(1) # 4.7 GB DVD
else:
self.wdisc_size.set_active(3) # 700 MB CD
if (self.disc_type == "dvd"):
self.winmenu.set_visible(True)
self.wframe_menu.show_all()
self.wcreate_menu.set_active(True)
else:
self.winmenu.set_visible(False)
self.wframe_menu.hide()
self.wcreate_menu.set_active(False)
if hasattr(self, "wprofile_combo"):
self._update_profile_visibility()
def get_current_file(self):
""" returns the currently selected file """
selection = self.wfiles.get_selection()
model, treeiter = selection.get_selected()
if treeiter is not None:
element = model[treeiter][0]
position = 0
for row in self.wfiles.get_model():
item = row.model[row.iter][0]
if element == item:
break
position += 1
return ((element, position, model, treeiter))
else:
return ((None, -1, None, None))
def get_all_files(self):
retval = []
for row in self.wfiles.get_model():
retval.append(row.model[row.iter][0])
return retval
def set_interface_status(self, b):
self.wadd_file.set_sensitive(True)
self.wdelete_file.set_sensitive(True)
self.wup_file.set_sensitive(True)
self.wdown_file.set_sensitive(True)
self.wproperties_file.set_sensitive(True)
self.wpreview_file.set_sensitive(True)
status = self.wcreate_menu.get_active()
self.wmenu_options.set_sensitive(status)
self.pal = self.wuse_pal.get_active()
(element, position, model, treeiter) = self.get_current_file()
if (element is None):
self.wdelete_file.set_sensitive(False)
self.wup_file.set_sensitive(False)
self.wdown_file.set_sensitive(False)
self.wproperties_file.set_sensitive(False)
self.wpreview_file.set_sensitive(False)
else:
if element.element_type != "file_movie":
self.wpreview_file.set_sensitive(False)
self.wproperties_file.set_sensitive(False)
nfiles = len(self.wfiles.get_model())
if (nfiles < 1):
self.wdelete_file.set_sensitive(False)
if (position == 0):
self.wup_file.set_sensitive(False)
if (position == (nfiles - 1)):
self.wdown_file.set_sensitive(False)
def on_cellrenderertext3_edited(self, widget, path, text):
element = self.wliststore_files[path][0]
element.set_name(text)
def on_cellrenderertext4_toggled(self, widget, path):
element = self.wliststore_files[path][0]
if element.element_type == "file_movie":
element.set_show_in_menu(False if widget.get_active() else True)
else:
element.set_page_jump(False if widget.get_active() else True)
def on_use_pal_toggled(self, b):
self.config.PAL = self.wuse_pal.get_active()
self.set_interface_status(None)
def on_wmain_window_delete_event(self, b, e=None):
ask = devedeng.ask.ask_window()
if (ask.run(_("Abort the current DVD and exit?"), _("Exit DeVeDe"))):
Gtk.main_quit()
return True
def title_changed(self, obj, new_title):
for item in self.wliststore_files:
element = item.model[item.iter][0]
if element == obj:
item.model.set_value(item.iter, 1, new_title)
self.refresh_disc_usage()
def in_menu_changed(self, obj, new_inmenu):
for item in self.wliststore_files:
element = item.model[item.iter][0]
if element == obj:
item.model.set_value(item.iter, 4, new_inmenu)
self.refresh_disc_usage()
def on_help_clicked(self, b):
self.help_file = devedeng.help.help("main.html")
def on_help_index_activate(self, b):
self.help_file = devedeng.help.help("index.html")
def on_add_file_clicked(self, b):
ask_files = devedeng.add_files.add_files()
if (ask_files.run()):
self.add_several_files(ask_files.files)
def duration_to_string(self, duration):
seconds = math.floor(duration % 60)
minutes = math.floor((duration / 60) % 60)
hours = math.floor(duration / 3600)
output = str(seconds) + "s"
if ((hours != 0) or (minutes != 0)):
output = str(minutes) + "m " + output
if (hours != 0):
output = str(hours) + "h " + output
return output
def add_several_files(self, file_list):
error_list = []
for efile in file_list:
if efile.startswith("file://"):
efile = urllib.parse.unquote(efile[7:])
new_file = devedeng.file_movie.file_movie(efile)
if (new_file.error):
error_list.append(os.path.basename(efile))
else:
new_file.connect('title_changed', self.title_changed)
new_file.connect('in_menu_changed', self.in_menu_changed)
self.wliststore_files.append([new_file, new_file.title_name, True, self.duration_to_string(
new_file.get_duration()), new_file.show_in_menu, _("In menu")])
if (len(error_list) != 0):
devedeng.message.message_window(_("The following files could not be added:"), _(
"Error while adding files"), error_list)
self.set_interface_status(None)
self.refresh_disc_usage()
def on_delete_file_clicked(self, b):
(element, position, model, treeiter) = self.get_current_file()
if (element is None):
return
if (element.element_type == "file_movie"):
ask_w = devedeng.ask.ask_window()
do_delete = ask_w.run(_("The file %(X)s (%(Y)s) will be removed.") % {"X": element.title_name, "Y": element.file_name}, _("Delete file"))
else:
do_delete = True
if do_delete:
element.delete_element()
model.remove(treeiter)
self.set_interface_status(None)
self.refresh_disc_usage()
def on_up_file_clicked(self, b):
(element, position, model, treeiter) = self.get_current_file()
if (element is None) or (position == 0):
return
last_element = self.wfiles.get_model()[position - 1]
self.wfiles.get_model().swap(last_element.iter, treeiter)
self.set_interface_status(None)
def on_down_file_clicked(self, b):
(element, position, model, treeiter) = self.get_current_file()
if (element is None) or (position == (len(self.wfiles.get_model()) - 1)):
return
last_element = self.wfiles.get_model()[position + 1]
self.wfiles.get_model().swap(last_element.iter, treeiter)
self.set_interface_status(None)
def on_properties_file_clicked(self, b):
(element, position, model, treeiter) = self.get_current_file()
if (element is None):
return
element.properties()
def on_create_menu_toggled(self, b):
self.set_interface_status(None)
self.refresh_disc_usage()
def on_adjust_disc_usage_clicked(self, b):
total_resolution = 0
fixed_size = 0
to_adjust = []
for f in self.get_all_files():
if f.element_type != "file_movie":
continue
estimated_size, videorate_fixed_size, audio_rate, sub_rate, width, height, time_length = f.get_size_data()
if videorate_fixed_size:
fixed_size += estimated_size
else:
fixed_size += ((audio_rate + sub_rate) * time_length)
# 76800 = 320x240, which is the smallest resolution
surface = ((float(width) * float(height)) /
76800.0) * float(time_length)
to_adjust.append((f, surface, time_length, audio_rate, height))
total_resolution += surface
if (self.disc_type == "dvd") and (self.wcreate_menu.get_active()):
fixed_size += self.menu.get_estimated_size() * 8.0
if (self.disc_type == "dvd"):
max_bps_base = devede_project.dvd_max_bps_base.copy()
min_bps = devede_project.dvd_min_bps
max_total = devede_project.dvd_max_total
elif (self.disc_type == "svcd") or (self.disc_type == "cvd"):
max_bps_base = devede_project.svcd_max_bps_base.copy()
min_bps = devede_project.svcd_min_bps
max_total = devede_project.svcd_max_total
elif (self.disc_type == "divx"):
max_bps_base = devede_project.divx_max_bps_base.copy()
min_bps = devede_project.divx_min_bps
max_total = devede_project.divx_max_total
elif (self.disc_type == "mkv") or (self.disc_type == "hevc"):
max_bps_base = devede_project.mkv_max_bps_base.copy()
min_bps = devede_project.mkv_min_bps
max_total = devede_project.mkv_max_total
else:
max_bps_base = devede_project.def_max_bps_base.copy()
min_bps = devede_project.def_min_bps
max_total = devede_project.def_max_total
if (total_resolution != 0):
remaining_disc_size = (
(8000.0 * self.get_dvd_size()[0]) - fixed_size) # in kbits
for l in to_adjust:
f = l[0]
surface = l[1]
length = l[2]
audio_rate = l[3]
height = l[4]
max_bps = max_bps_base[0]
resy = 0
for bitrates in max_bps_base:
if (height >= bitrates) and (resy < bitrates):
resy = bitrates
max_bps = max_bps_base[bitrates]
video_rate = (remaining_disc_size * surface) / \
(length * total_resolution)
if max_total != -1:
max2 = max_total - audio_rate
if (max2 > max_bps):
max2 = max_bps
else:
max2 = max_bps
if (video_rate > max2):
video_rate = max2
if (video_rate < min_bps):
video_rate = min_bps
f.set_auto_video_audio_rate(video_rate, audio_rate)
self.refresh_disc_usage()
def refresh_disc_usage(self):
used = 0.0
for f in self.get_all_files():
if f.element_type != "file_movie":
continue
estimated_size = f.get_estimated_size()
used += float(estimated_size)
if self.wcreate_menu.get_active():
used += float(self.menu.get_estimated_size())
used /= 1000.0
disc_size, minvrate, maxvrate = self.get_dvd_size()
if used > disc_size:
self.wdisc_fill_level.set_fraction(1.0)
addv = 1
else:
self.wdisc_fill_level.set_fraction(used / disc_size)
addv = 0
self.wdisc_fill_level.set_text(
str(addv + int((used / disc_size) * 100)) + "%")
self.wdisc_fill_level.set_show_text(True)
def on_menu_options_clicked(self, b):
self.menu.show_configuration(self.get_all_files())
def get_dvd_size(self):
""" Returns the size for the currently selected disk type, and the minimum and maximum
videorate for the current video disk """
active = self.wdisc_size.get_active()
# here we choose the size in Mbytes for the media
if 5 == active:
size = 170.0
elif 4 == active:
size = 700.0
elif 3 == active:
size = 750.0
elif 2 == active:
size = 1100.0
elif 1 == active:
size = 4200.0
else:
size = 8000.0
if self.disc_type == "vcd":
minvrate = 1152
maxvrate = 1152
elif (self.disc_type == "svcd") or (self.disc_type == "cvd"):
minvrate = 400
maxvrate = 2300
elif (self.disc_type == "dvd"):
minvrate = 400
maxvrate = 8500
elif (self.disc_type == "divx") or (self.disc_type == "mkv") or (self.disc_type == "hevc"):
minvrate = 300
maxvrate = 6000
else:
minvrate = 0
maxvrate = 8000
size *= 0.90 # a safe margin of 10% to ensure that it never will be bigger
# (it's important to have in mind the space needed by disk structures like
# directories, file entries, and so on)
return size, minvrate, maxvrate
def on_disc_size_changed(self, c):
self.refresh_disc_usage()
def plan_disc_split(self):
""" Builds a list of "disc plans" so the project fits across as many
discs as needed. Each disc plan is a list of segments:
(movie, start_sec, duration_sec)
where duration_sec == 0 means "whole remaining movie".
A title that fits is placed whole on the current disc (starting a
new disc if it would overflow). A single title bigger than one disc
is split by time at the chapter boundary nearest each disc's
capacity (falling back to an even time cut when it has no chapters).
"""
# per-disc capacity in kBytes (get_dvd_size already applies a margin)
disc_capacity = self.get_dvd_size()[0] * 1000.0
# menu lives on the first disc only; reserve its space there
first_disc_reserved = 0.0
if (self.disc_type == "dvd") and self.wcreate_menu.get_active():
first_disc_reserved = float(self.menu.get_estimated_size())
movies = [m for m in self.get_all_files()
if m.element_type == "file_movie"]
plan = plan_disc_split(movies, disc_capacity, first_disc_reserved)
return plan
def apply_high_profile_bitrate(self):
""" For the High-quality profile: compute the fill-the-disc video
bitrate (fewest discs at the cap, then fill) and apply it to every
movie, and force 2-pass encoding for accurate VBR. After this the
normal split planner produces the right number of discs because the
content now fills them. No-op for non-DVD or other profiles. """
if self.disc_type != "dvd":
return
if getattr(self.config, "encode_profile", "compatibility") != "high":
return
movies = [m for m in self.get_all_files()
if m.element_type == "file_movie"]
if not movies:
return
total_runtime = 0.0
total_audio_kbits = 0.0
for m in movies:
length = float(m.original_length)
total_runtime += length
audio_streams = max(1, getattr(m, "audio_streams", 1))
sub_rate = 8 * len(getattr(m, "subtitles_list", []))
# High profile uses AC3 448k audio
total_audio_kbits += (448 * audio_streams + sub_rate) * length
disc_capacity = self.get_dvd_size()[0] * 1000.0 # kBytes
menu_kbytes = 0.0
if self.wcreate_menu.get_active():
menu_kbytes = float(self.menu.get_estimated_size())
n_discs, video_bitrate = compute_high_profile_bitrate(
total_runtime, total_audio_kbits, disc_capacity, menu_kbytes)
for m in movies:
m.set_auto_video_audio_rate(video_bitrate, 448)
m.video_rate_automatic = True # use the computed auto rate
m.two_pass_encoding = True # VBR accuracy
self.refresh_disc_usage()
def _build_profile_selector(self):
""" Build the DVD encode-profile selector (Compatibility / High quality
/ Custom) and pack it next to the disc-size combo. When "Custom" is
chosen, two spin buttons for video/audio kbps become visible. """
# ordered to match the config string values
self._profile_order = ["compatibility", "high", "custom"]
labels = [_("Compatibility (4500k, old players)"),
_("High quality (fill disc)"),
_("Custom bitrate")]
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
box.set_margin_top(4)
box.set_margin_bottom(4)
box.pack_start(Gtk.Label(label=_("Encode profile:")), False, True, 0)
self.wprofile_combo = Gtk.ComboBoxText()
for text in labels:
self.wprofile_combo.append_text(text)
try:
active = self._profile_order.index(self.config.encode_profile)
except ValueError:
active = 0
self.wprofile_combo.set_active(active)
self.wprofile_combo.connect("changed", self.on_profile_changed)
box.pack_start(self.wprofile_combo, False, True, 0)
# custom bitrate spinners (hidden unless Custom is active)
self.wcustom_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
spacing=4)
self.wcustom_box.pack_start(Gtk.Label(label=_("Video kbps:")),
False, True, 0)
self.wcustom_video = Gtk.SpinButton.new_with_range(1000, 9800, 100)
self.wcustom_video.set_value(self.config.custom_video_kbps)
self.wcustom_video.connect("value-changed", self.on_custom_rate_changed)
self.wcustom_box.pack_start(self.wcustom_video, False, True, 0)
self.wcustom_box.pack_start(Gtk.Label(label=_("Audio kbps:")),
False, True, 0)
self.wcustom_audio = Gtk.SpinButton.new_with_range(64, 448, 32)
self.wcustom_audio.set_value(self.config.custom_audio_kbps)
self.wcustom_audio.connect("value-changed", self.on_custom_rate_changed)
self.wcustom_box.pack_start(self.wcustom_audio, False, True, 0)
box.pack_start(self.wcustom_box, False, True, 0)
box.show_all()
self.wcustom_box.set_visible(
self.config.encode_profile == "custom")
# pack above the disc-size combo, in its container
parent = self.wdisc_size.get_parent()
if parent is not None:
parent.pack_start(box, False, True, 0)
try:
parent.reorder_child(box, 0)
except Exception:
pass
self._update_profile_visibility()
def _update_profile_visibility(self):
""" The profile only applies to DVD; hide the selector otherwise. """
is_dvd = (self.disc_type == "dvd")
if hasattr(self, "wprofile_combo"):
self.wprofile_combo.get_parent().set_visible(is_dvd)
def on_profile_changed(self, combo):
idx = combo.get_active()
if 0 <= idx < len(self._profile_order):
self.config.encode_profile = self._profile_order[idx]
self.wcustom_box.set_visible(self.config.encode_profile == "custom")
self.refresh_disc_usage()
def on_custom_rate_changed(self, spin):
self.config.custom_video_kbps = int(self.wcustom_video.get_value())
self.config.custom_audio_kbps = int(self.wcustom_audio.get_value())
self.refresh_disc_usage()
def _ask_split_choice(self, n_discs):
""" Ask the user how to handle a project that exceeds one disc.
Returns "split", "reduce", or "cancel". """
dialog = Gtk.MessageDialog(
transient_for=self.wmain_window,
modal=True,
message_type=Gtk.MessageType.QUESTION,
text=_("This project does not fit on a single disc."))
dialog.format_secondary_text(
_("At the current quality it needs %d discs. You can split it "
"into several discs (keeping quality) or reduce the bitrate to "
"fit a single disc.") % n_discs)
dialog.add_button(_("Cancel"), 1)
dialog.add_button(_("Reduce bitrate"), 2)
dialog.add_button(_("Split into %d discs") % n_discs, 3)
response = dialog.run()
dialog.destroy()
if response == 3:
return "split"
if response == 2:
return "reduce"
return "cancel"
def _build_split_dvd(self, run_window, data, split_plan):
""" Author each disc of a split DVD project into its own subfolder and
emit one ISO per disc (name-disc1.iso, name-disc2.iso, ...).
If the project has a menu enabled, each disc gets its own menu
listing that disc's titles; otherwise each disc auto-plays its
segment(s). """
cv = devedeng.converter.converter.get_converter()
title = self.menu.title_text if self.menu.title_text else ""
title = title.strip()
menuWide = (self.menu.menu_aspect_ratio == "menu_aspect_16_9")
with_menu = (self.disc_type == "dvd") and self.wcreate_menu.get_active()
start_with_menu = with_menu and (
self.menu.at_startup == "menu_show_at_startup")
play_all = self.menu.play_all_c
last_iso = None
for disc_index, segments in enumerate(split_plan):
disc_no = disc_index + 1
disc_path = os.path.join(data.path, "disc{:d}".format(disc_no))
movie_folder = os.path.join(disc_path, "movies")
try:
os.makedirs(movie_folder)
except OSError:
pass
deps = []
seg_movies = []
segment_lengths = []
for seg_index, (movie, start, duration) in enumerate(segments):
out = os.path.join(
movie_folder, "movie_{:d}.mpg".format(seg_index))
p = movie.do_conversion(out, duration=duration,
start_offset=start)
run_window.add_process(p)
deps.append(p)
seg_movies.append(movie)
# effective length of this segment for chapter math: an explicit
# duration, or whole-movie-from-start when duration is 0
if duration and duration > 0:
segment_lengths.append(int(duration))
elif start > 0:
segment_lengths.append(int(movie.original_length - start))
else:
segment_lengths.append(None) # whole, unsplit movie
# build this disc's own menu from its titles, if enabled
menu_entries = None
if with_menu:
menu_procs, menu_entries = self.menu.create_dvd_menus(
seg_movies, disc_path)
for mp in menu_procs:
run_window.add_process(mp)
deps.append(mp)
dvdauthor = devedeng.dvdauthor_converter.dvdauthor_converter()
dvdauthor.create_dvd_project(disc_path,
"{:s}-disc{:d}".format(data.name, disc_no),
seg_movies, menu_entries,
start_with_menu, play_all, menuWide,
segment_lengths)
for d in deps:
dvdauthor.add_dependency(d)
run_window.add_process(dvdauthor)
isocreator = cv.get_mkiso()()
iso_name = "{:s}-disc{:d}".format(data.name, disc_no)
isocreator.create_iso(disc_path, iso_name, title)
isocreator.add_dependency(dvdauthor)
run_window.add_process(isocreator)
last_iso = os.path.join(disc_path, iso_name + ".iso")
# burning multiple discs interactively isn't automated; point the
# "done" handler at the last image and let the user burn each in turn
self.disc_image_name = last_iso
def on_create_disc_clicked(self, b):
if self.disc_type == "dvd":
max_files = 62
else:
max_files = -1
file_movies = self.get_all_files()
t = 0
for element in file_movies:
if element.element_type == "file_movie":
t += 1
if (max_files != -1) and (t > max_files):
devedeng.message.message_window(_("The limit for this format is %(l)d files, but your project has %(h)d.") % {"l": max_files, "h": t}, _("Too many files in the project"))
return
data = devedeng.create_disk_window.create_disk_window()
if (not data.run()):
return
if os.path.exists(data.path):
ask_w = devedeng.ask.ask_window()
retval = ask_w.run(_("The selected folder already exists. To create the project, Devede must delete it.\nIf you continue, the folder\n\n %s\n\n and all its contents will be deleted. Continue?") % data.path, _("Delete folder"))
if retval:
# delete only the bare minimun needed
shutil.rmtree(os.path.join(data.path, "dvd_tree"), True)
shutil.rmtree(os.path.join(data.path, "menu"), True)
shutil.rmtree(os.path.join(data.path, "movies"), True)
shutil.rmtree(os.path.join(data.path, "xml_data"), True)
if self.config.disc_type == "dvd":
try:
os.unlink(os.path.join(data.path, data.name + ".iso"))
except OSError:
pass
if (self.config.disc_type == "vcd") or (self.config.disc_type == "svcd") or (self.config.disc_type == "cvd"):
try:
os.unlink(os.path.join(data.path, data.name + ".bin"))
except OSError:
pass
try:
os.unlink(os.path.join(data.path, data.name + ".cue"))
except OSError:
pass
else:
return
self.shutdown = data.shutdown
# For DVD, decide whether the project needs more than one disc. The
# other formats keep their original single-image behaviour.
split_plan = None
if self.disc_type == "dvd":
# High profile: derive the fill-the-disc bitrate first so the split
# planner sees the real per-disc sizes (this also enables 2-pass)
self.apply_high_profile_bitrate()
plan = self.plan_disc_split()
if len(plan) > 1:
choice = self._ask_split_choice(len(plan))
if choice == "cancel":
return
if choice == "reduce":
# squeeze the bitrate so everything fits one disc
self.on_adjust_disc_usage_clicked(None)
plan = self.plan_disc_split()
# if it still doesn't fit, fall back to splitting
split_plan = plan if len(plan) > 1 else None
else: # "split"
split_plan = plan
run_window = devedeng.runner.runner()
if (self.disc_type == "dvd") and (split_plan is not None):
self._build_split_dvd(run_window, data, split_plan)
run_window.connect("done", self.disc_done)
self.wmain_window.hide()
self.time_start = time.time()
run_window.run()
return
final_dependencies = []
if (self.disc_type == "dvd") and (self.wcreate_menu.get_active()):
processes, menu_entries = self.menu.create_dvd_menus(file_movies, data.path)
for p in processes:
run_window.add_process(p)
final_dependencies.append(p)
else:
menu_entries = None
movie_folder = os.path.join(data.path, "movies")
try:
os.makedirs(movie_folder)
except OSError:
pass
counter = 0
if self.disc_type == "divx":
extension = "avi"
elif (self.disc_type == "mkv") or (self.disc_type == "hevc"):
extension = "mkv"
else:
extension = "mpg"
for movie in file_movies:
if movie.element_type != "file_movie":
continue
p = movie.do_conversion(os.path.join(
movie_folder, "movie_{:d}.{:s}".format(counter, extension)))
run_window.add_process(p)
final_dependencies.append(p)
counter += 1
if (self.disc_type == "dvd"):
if (self.menu.at_startup == "menu_show_at_startup"):
start_with_menu = True
else:
start_with_menu = False
dvdauthor = devedeng.dvdauthor_converter.dvdauthor_converter()
dvdauthor.create_dvd_project(data.path, data.name, file_movies, menu_entries, start_with_menu, self.menu.play_all_c, self.menu.menu_aspect_ratio == "menu_aspect_16_9")
# dvdauthor must wait until all the files have been converted
for element in final_dependencies:
dvdauthor.add_dependency(element)
run_window.add_process(dvdauthor)
cv = devedeng.converter.converter.get_converter()
isocreator = cv.get_mkiso()()
title = self.menu.title_text
if title is None:
title = ""
isocreator.create_iso(data.path, data.name, title.strip())
isocreator.add_dependency(dvdauthor)
run_window.add_process(isocreator)
self.disc_image_name = os.path.join(data.path, data.name + ".iso")
elif (self.disc_type == "vcd") or (self.disc_type == "svcd") or (self.disc_type == "cvd"):
vcdcreator = devedeng.vcdimager_converter.vcdimager_converter()
vcdcreator.create_cd_project(data.path, data.name, file_movies)
for element in final_dependencies:
vcdcreator.add_dependency(element)
run_window.add_process(vcdcreator)
self.disc_image_name = os.path.join(data.path, data.name + ".cue")
else:
self.disc_image_name = None
run_window.connect("done", self.disc_done)
self.wmain_window.hide()
self.time_start = time.time()
run_window.run()
def disc_done(self, object, value):
if self.shutdown:
Gtk.main_quit()
devedeng.shutdown.shutdown()
if value == 0:
ended = devedeng.end_job.end_window()
if self.disc_image_name is None:
do_burn = False
else:
do_burn = True
if (ended.run(time.time() - self.time_start, do_burn)):
cv = devedeng.converter.converter.get_converter()
burner = cv.get_burner()()
burner.burn(self.disc_image_name)
run_window = devedeng.runner.runner(False)
run_window.add_process(burner)
run_window.connect("done", self.disc_done2)
run_window.run()
return
self.wmain_window.show()
def disc_done2(self, object, value):
self.wmain_window.show()
def on_preview_file_clicked(self, b):
(element, position, model, treeiter) = self.get_current_file()
if (element is None):
return
element.do_preview()
def on_settings_activate(self, b):
w = devedeng.settings.settings_window(self.wmain_window)
def on_about_activate(self, b):
w = devedeng.about.about_window()
def on_new_activate(self, b):
w = devedeng.ask.ask_window()
if w.run(_("Close current project and start a fresh one?"), _("New project")):
self.wliststore_files.clear()
self.project_file = None
self.wmain_window.hide()
devedeng.choose_disc_type.choose_disc_type()
def on_wmain_window_drag_motion(self, wid, context, x, y, time):
Gdk.drag_status(context, Gdk.DragAction.COPY, time)
return True
def on_wmain_window_drag_drop(self, wid, context, x, y, time):
# Used with windows drag and drop
return True
def on_wmain_window_drag_data_received(self, widget, drag_context, x, y, data, info, time):
uris = data.get_uris()
self.add_several_files(uris)
Gtk.drag_finish(drag_context, True, Gdk.DragAction.COPY, time)
def on_save_activate(self, b):
if self.project_file is not None:
self.save_current_project()
else:
self.on_save_as_activate(None)
def on_save_as_activate(self, b):
while True:
w = devedeng.opensave.opensave_window(True)
retval = w.run(self.project_file)
if retval is None:
return
if not retval.endswith(".devedeng"):
retval += ".devedeng"
if os.path.isfile(retval):
w = devedeng.ask.ask_window()
if not w.run(_("The file already exists. Overwrite it?"), _("The file already exists")):
continue
self.project_file = retval
self.save_current_project()
return
def on_load_activate(self, b):
w = devedeng.opensave.opensave_window(False)
retval = w.run()
if retval is not None:
self.load_project(retval)
def save_current_project(self):
project = {}
project["PAL"] = self.wuse_pal.get_active()
project["create_menu"] = self.wcreate_menu.get_active()
project["disc_type"] = self.config.disc_type
project["disc_size"] = self.wdisc_size.get_active()
project["files"] = []
for i in self.get_all_files():
project["files"].append(i.store_element())
if self.disc_type == "dvd":
project["menu"] = self.menu.store_menu()
with open(self.project_file, 'wb') as f:
pickle.dump(project, f, 3)
recent_mgr = Gtk.RecentManager.get_default()
uri = GLib.filename_to_uri(self.project_file)
recent_mgr.add_item(uri)
def load_project(self, project_file):
self.wliststore_files.clear()
self.project_file = project_file
with open(project_file, 'rb') as f:
project = pickle.load(f)
if "disc_type" in project:
self.config.set_disc_type(project["disc_type"])
if "PAL" in project:
self.wuse_pal.set_active(project["PAL"])
if "create_menu" in project:
self.wcreate_menu.set_active(project["create_menu"])
if "disc_size" in project:
self.wdisc_size.set_active(project["disc_size"])
if "menu" in project:
self.menu.restore_menu(project["menu"])
if "files" in project:
error_list = []
for element in project["files"]:
if not "element_type" in element:
element["element_type"] = "file_movie"
if element["element_type"] == "file_movie":
new_file = devedeng.file_movie.file_movie(element["file_name"])
if (new_file.error):
error_list.append(os.path.basename(element["file_name"]))
continue
new_file.restore_element(element)
new_file.connect('title_changed', self.title_changed)
new_file.connect('in_menu_changed', self.in_menu_changed)
self.wliststore_files.append([new_file, new_file.title_name, True, self.duration_to_string(
new_file.get_duration()), new_file.show_in_menu, _("In menu")])
else:
new_separator = devedeng.separator.separator()
new_separator.restore_element(element)
new_separator.connect('name_changed', self.title_changed)
new_separator.connect('page_jump_changed', self.in_menu_changed)
self.wliststore_files.append([new_separator, new_separator.separator_name, True, "", new_separator.page_jump, _("Page jump")])
if (len(error_list) != 0):
devedeng.message.message_window(_("The following files in the project could not be added again:"), _("Error while adding files"), error_list)
self.set_interface_status(None)
self.refresh_disc_usage()
recent_mgr = Gtk.RecentManager.get_default()
uri = GLib.filename_to_uri(self.project_file)
recent_mgr.add_item(uri)
def on_multiproperties_activate(self, b):
file_list = []
for f in self.get_all_files():
if f.element_type != "file_movie":
continue
file_list.append(f)
e = devedeng.file_movie.file_movie(None, file_list)
e.properties()
def on_add_separator_clicked(self, b):
new_separator = devedeng.separator.separator()
new_separator.connect('name_changed', self.title_changed)
new_separator.connect('page_jump_changed', self.in_menu_changed)
self.wliststore_files.append([new_separator, new_separator.separator_name, True, "", False, _("Page jump")])
self.set_interface_status(None)
self.refresh_disc_usage()