# 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()