# 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 as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# 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
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")
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"):
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)
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"):
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"):
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 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:
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:
pass
try:
os.unlink(os.path.join(data.path, data.name + ".cue"))
except:
pass
else:
return
self.shutdown = data.shutdown
run_window = devedeng.runner.runner()
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:
pass
counter = 0
if self.disc_type == "divx":
extension = "avi"
elif self.disc_type == "mkv":
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()