diff --git a/dvd_ripper.py b/dvd_ripper.py index a2454ff..ece53fa 100644 --- a/dvd_ripper.py +++ b/dvd_ripper.py @@ -2,37 +2,64 @@ import cffi import os import sys import time -from dvdnav import DVDNav +from dvdnav import DVDNav,DVDError from dvdread import DVDRead import subprocess as SP import json +from glob import glob +import itertools as ITT from vob_demux import demux from ff_d2v import make_d2v -def loadlib(dll_path,*includes,**kwargs): + +def loadlib(dll_path, *includes, **kwargs): ffi = cffi.FFI() for include in includes: - ffi.cdef(open(include).read(),kwargs) - return ffi,ffi.dlopen(dll_path) + ffi.cdef(open(include).read(), kwargs) + return ffi, ffi.dlopen(dll_path) -os.environ["DVDCSS_VERBOSE"]="2" -os.environ["DVDCSS_METHOD"]="disc" +for dvd_path in ITT.chain.from_iterable(map(glob,sys.argv[1:])): + r = DVDRead(dvd_path) + # r.grab_ifos() + # r.grab_vobs() + # exit() -r=DVDRead(sys.argv[1]) -out_folder=os.path.join("out","_".join([r.disc_id,r.udf_disc_name or r.iso_disc_name]).replace(" ","_")) -del r -os.makedirs(out_folder,exist_ok=True) -d=DVDNav(sys.argv[1]) -for k,v in d.titles.items(): - v['duration']=v['duration'].total_seconds() - v['chapters']=[c.total_seconds() for c in v['chapters']] - d.titles[k]=v - with open(os.path.join(out_folder,f"{k}.json"),"w") as fh: - json.dump(v,fh) - for a in range(v['angles']): - a+=1 - outfile=os.path.join(out_folder,f"{k}_{a}.vob") - with open(outfile,"wb") as fh: - for block in d.get_blocks(k,a): - fh.write(block) - demux(outfile) - os.unlink(outfile) \ No newline at end of file + out_folder = os.path.join( + "out", "_".join([r.disc_id, r.udf_disc_name or r.iso_disc_name]).replace(" ", "_") + ) + os.makedirs(out_folder, exist_ok=True) + d = DVDNav(dvd_path) + to_demux = [] + for k, v in d.titles.items(): + v["duration"] = v["duration"].total_seconds() + v["chapters"] = [c.total_seconds() for c in v["chapters"]] + d.titles[k] = v + with open(os.path.join(out_folder, f"{k:03}.json"), "w") as fh: + json.dump(v, fh) + for a in range(0,99): + block=0 + outfile = os.path.join(out_folder, f"t{k:03}_a{a:03}_b{block:03}.vob") + to_demux.append(outfile) + fh = open(outfile, "wb") + try: + for block in d.get_blocks(k, a): + if isinstance(block, int): + outfile = os.path.join(out_folder, f"t{k:03}_a{a:03}_b{block:03}.vob") + to_demux.append(outfile) + if fh: + fh.close() + fh = open(outfile, "wb") + else: + fh.write(block) + except DVDError as e: + if str(e)!="Invalid angle specified!": + raise + if fh.tell()==0: + fh.close() + os.unlink(fh.name) + while fh.name in to_demux: + to_demux.remove(fh.name) + for file in to_demux: + demux(file) + os.unlink(file) + for file in glob(os.path.join(out_folder,"*.m2v")): + make_d2v(file) diff --git a/dvdnav.py b/dvdnav.py index fe8102c..4a78fe9 100644 --- a/dvdnav.py +++ b/dvdnav.py @@ -3,58 +3,77 @@ import os import functools from datetime import timedelta from tqdm import tqdm +from dvdread import DVDRead -def loadlib(dll_path,*includes,**kwargs): + +def loadlib(dll_path, *includes, **kwargs): ffi = cffi.FFI() for include in includes: - ffi.cdef(open(include).read(),kwargs) - return ffi,ffi.dlopen(dll_path) + ffi.cdef(open(include).read(), kwargs) + return ffi, ffi.dlopen(dll_path) + class DVDError(Exception): pass + class DVDNav(object): - def __init__(self,path,verbose=2,method="disc"): - os.environ["DVDCSS_VERBOSE"]=str(verbose) - os.environ["DVDCSS_METHOD"]=method - self.dvd=None - self.ffi,self.lib = loadlib("libdvdnav-4.dll", + def __init__(self, path, verbose=None, method="disc"): + if verbose is None: + os.environ.pop("DVDCSS_VERBOSE", None) + else: + os.environ["DVDCSS_VERBOSE"] = str(verbose) + os.environ["DVDCSS_METHOD"] = method + self.dvd = None + self.ffi, self.lib = loadlib( + "libdvdnav-4.dll", "dvd_types.h", "dvd_reader.h", "ifo_types.h", "nav_types.h", "dvdnav_events.h", - "dvdnav.h") - self.path=path - self.titles={} + "dvdnav.h", + pack=True, + ) + self.path = path + self.titles = {} self.open(path) def __del__(self): self.__check_error(self.lib.dvdnav_close(self.dvd)) - self.dvd=None + self.dvd = None def __repr__(self): return " num_angles[0]: + raise DVDError("Invalid angle specified!") + if angle != curr_angle[0]: + self.__check_error(self.lib.dvdnav_angle_change(self.dvd, angle)) if slang is not None: - self.__check_error(self.lib.dvdnav_spu_language_select(self.dvd,slang)) - event=self.lib.DVDNAV_NOP - buf=self.ffi.new("char[]",4096) - ev=self.ffi.new("int32_t*",self.lib.DVDNAV_NOP) - size=self.ffi.new("int32_t*",0) - pos=self.ffi.new("uint32_t*",0) - total_size=self.ffi.new("uint32_t*",0) + self.__check_error(self.lib.dvdnav_spu_language_select(self.dvd, slang)) + event = self.lib.DVDNAV_NOP + buf = self.ffi.new("char[]", 4096) + ev = self.ffi.new("int32_t*", self.lib.DVDNAV_NOP) + size = self.ffi.new("int32_t*", 0) + pos = self.ffi.new("uint32_t*", 0) + total_size = self.ffi.new("uint32_t*", 0) domains = { 1: "FirstPlay", 2: "VTSTitle", 4: "VMGM", - 8: "VTSMenu" + 8: "VTSMenu", } - events={ + events = { 0: "DVDNAV_BLOCK_OK", 1: "DVDNAV_NOP", 2: "DVDNAV_STILL_FRAME", @@ -67,146 +86,180 @@ class DVDNav(object): 9: "DVDNAV_HIGHLIGHT", 10: "DVDNAV_SPU_CLUT_CHANGE", 12: "DVDNAV_HOP_CHANNEL", - 13: "DVDNAV_WAIT" + 13: "DVDNAV_WAIT", } - progbar=tqdm(unit_divisor=1024,unit_scale=True,unit="iB",desc="Ripping DVD") + progbar = tqdm( + unit_divisor=1024, + unit_scale=True, + unit="iB", + desc="Ripping DVD", + disable=False, + ) + ripped = set() + current_vts = None + current_cell = None + current_pg = None while True: - self.__check_error(self.lib.dvdnav_get_next_block(self.dvd,buf,ev,size)) - if self.lib.dvdnav_get_position(self.dvd,pos,total_size)==self.lib.DVDNAV_STATUS_OK: - progbar.total=total_size[0]*2048 - progbar.n=max(progbar.n,min(progbar.total,pos[0]*2048)) + self.__check_error(self.lib.dvdnav_get_next_block(self.dvd, buf, ev, size)) + if ( + self.lib.dvdnav_get_position(self.dvd, pos, total_size) + == self.lib.DVDNAV_STATUS_OK + ): + progbar.total = total_size[0] * 2048 + progbar.n = max(progbar.n, min(progbar.total, pos[0] * 2048)) progbar.update(0) + progbar.set_postfix( + vts=current_vts, + cell=current_cell, + pg=current_pg, + angle=angle, + title=title, + ) # print("Got event:",events.get(ev[0],ev[0]),size[0]) - if ev[0] in [self.lib.DVDNAV_SPU_CLUT_CHANGE,self.lib.DVDNAV_HOP_CHANNEL,self.lib.DVDNAV_NOP]: + if ev[0] in [ + self.lib.DVDNAV_SPU_CLUT_CHANGE, + self.lib.DVDNAV_HOP_CHANNEL, + self.lib.DVDNAV_NOP, + self.lib.DVDNAV_HIGHLIGHT, + ]: continue - elif ev[0]==self.lib.DVDNAV_BLOCK_OK: - # print("Read",size[0]) - yield self.ffi.buffer(buf,size[0])[:] - elif ev[0]==self.lib.DVDNAV_STOP: + elif ev[0] == self.lib.DVDNAV_BLOCK_OK: + yield self.ffi.buffer(buf, size[0])[:] + elif ev[0] == self.lib.DVDNAV_STOP: + progbar.write(f"[{title}|{angle}] Stop") break - elif ev[0]==self.lib.DVDNAV_NAV_PACKET: - nav=self.lib.dvdnav_get_current_nav_pci(self.dvd) - # print("PTS:",timedelta(seconds=nav.pci_gi.vobu_s_ptm/90000)) - elif ev[0]==self.lib.DVDNAV_STILL_FRAME: - # print("Still") + elif ev[0] == self.lib.DVDNAV_NAV_PACKET: + pass + elif ev[0] == self.lib.DVDNAV_STILL_FRAME: self.__check_error(self.lib.dvdnav_still_skip(self.dvd)) - elif ev[0]==self.lib.DVDNAV_WAIT: - # print("Wait",size[0]) + elif ev[0] == self.lib.DVDNAV_WAIT: self.__check_error(self.lib.dvdnav_wait_skip(self.dvd)) - elif ev[0]==self.lib.DVDNAV_SPU_STREAM_CHANGE: - spu=self.ffi.cast("dvdnav_spu_stream_change_event_t*",buf) - progbar.write(f"[{title}|{angle}] SPU: Wide: {spu.physical_wide} Letterbox: {spu.physical_letterbox} Pan&Scan: {spu.physical_pan_scan} Logical: {spu.logical}") - elif ev[0]==self.lib.DVDNAV_AUDIO_STREAM_CHANGE: - audio=self.ffi.cast("dvdnav_audio_stream_change_event_t*",buf) - progbar.write(f"[{title}|{angle}] Audio: Physical: {audio.physical} Logical: {audio.logical}") - elif ev[0]==self.lib.DVDNAV_CELL_CHANGE: - cell=self.ffi.cast("dvdnav_cell_change_event_t*",buf) - progbar.write(f"[{title}|{angle}] Cell: {cell.cellN} ({cell.cell_start}-{cell.cell_start+cell.cell_length}), PG: {cell.pgN} ({cell.pg_start}-{cell.pg_start+cell.pg_length})") - elif ev[0]==self.lib.DVDNAV_VTS_CHANGE: - vts=self.ffi.cast("dvdnav_vts_change_event_t*",buf) - old_domain=sorted(domains[k] for k in domains if vts.old_domain&k) - new_domain=sorted(domains[k] for k in domains if vts.new_domain&k) - progbar.write(f"[{title}|{angle}] VTS: {vts.old_vtsN} ({vts.old_domain} {old_domain}) -> {vts.new_vtsN} ({vts.new_domain} {new_domain})") - if vts.new_domain==8: # back to menu + elif ev[0] == self.lib.DVDNAV_SPU_STREAM_CHANGE: + pass + elif ev[0] == self.lib.DVDNAV_AUDIO_STREAM_CHANGE: + audio = self.ffi.cast("dvdnav_audio_stream_change_event_t*", buf) + elif ev[0] == self.lib.DVDNAV_CELL_CHANGE: + cell = self.ffi.cast("dvdnav_cell_change_event_t*", buf) + current_cell = cell.cellN + current_pg = cell.pgN + progbar.write( + f"[{title}|{angle}] Cell: {cell.cellN} ({cell.cell_start}-{cell.cell_start+cell.cell_length}), PG: {cell.pgN} ({cell.pg_start}-{cell.pg_start+cell.pg_length})" + ) + elif ev[0] == self.lib.DVDNAV_VTS_CHANGE: + vts = self.ffi.cast("dvdnav_vts_change_event_t*", buf) + new_vts = (vts.new_vtsN, vts.new_domain) + ripped.add((vts.old_vtsN, vts.old_domain)) + # progbar.write(f"[{title}|{angle}] VTS: {vts.old_vtsN} ({vts.old_domain} {old_domain}) -> {vts.new_vtsN} ({vts.new_domain} {new_domain})") + if new_vts in ripped: # looped + progbar.write(f"[{title}|{angle}] Looped!") break + current_vts = (vts.new_vtsN, vts.new_domain) + if vts.new_domain == 8: # back to menu + progbar.write(f"[{title}|{angle}] Back to menu!") + break + yield vts.new_vtsN else: - progbar.write(f"[{title}|{angle}] Unhandled: {events.get(ev[0],ev[0])} {size[0]}") + progbar.write( + f"[{title}|{angle}] Unhandled: {events.get(ev[0],ev[0])} {size[0]}" + ) + self.__check_error(self.lib.dvdnav_stop(self.dvd)) - - def __check_error(self,ret): - if ret==self.lib.DVDNAV_STATUS_ERR: + def __check_error(self, ret): + if ret == self.lib.DVDNAV_STATUS_ERR: if self.dvd: - err=self.ffi.string(self.lib.dvdnav_err_to_string(self.dvd)) + err = self.ffi.string(self.lib.dvdnav_err_to_string(self.dvd)) raise DVDError(err) raise DVDError("Unknown error") def __get_titles(self): - titles=self.ffi.new("int32_t*",0) - p_times=self.ffi.new("uint64_t[]",512) - times=self.ffi.new("uint64_t**",p_times) - duration=self.ffi.new("uint64_t*",0) - titles=self.ffi.new("int32_t*",0) - self.lib.dvdnav_get_number_of_titles(self.dvd,titles) - num_titles=titles[0] - for title in range(0,num_titles+1): - if self.lib.dvdnav_get_number_of_parts(self.dvd,title,titles)==0: + titles = self.ffi.new("int32_t*", 0) + p_times = self.ffi.new("uint64_t[]", 512) + times = self.ffi.new("uint64_t**", p_times) + duration = self.ffi.new("uint64_t*", 0) + titles = self.ffi.new("int32_t*", 0) + self.lib.dvdnav_get_number_of_titles(self.dvd, titles) + num_titles = titles[0] + for title in range(0, num_titles + 1): + if self.lib.dvdnav_get_number_of_parts(self.dvd, title, titles) == 0: continue - num_parts=titles[0] - self.lib.dvdnav_get_number_of_angles(self.dvd,title,titles) - num_angles=titles[0] - num_chapters=self.lib.dvdnav_describe_title_chapters(self.dvd,title,times,duration) - if duration[0]==0: + num_parts = titles[0] + self.lib.dvdnav_get_number_of_angles(self.dvd, title, titles) + num_angles = titles[0] + num_chapters = self.lib.dvdnav_describe_title_chapters( + self.dvd, title, times, duration + ) + if duration[0] == 0: continue - chapters=[] + chapters = [] for t in range(num_chapters): - chapters.append(timedelta(seconds=times[0][t]/90000)) - self.titles[title]={ - 'parts':num_parts, - 'angles': num_angles, - 'duration': timedelta(seconds=duration[0]/90000), - 'chapters': chapters + chapters.append(timedelta(seconds=times[0][t] / 90000)) + self.titles[title] = { + "parts": num_parts, + "angles": num_angles, + "duration": timedelta(seconds=duration[0] / 90000), + "chapters": chapters, } def __get_info(self): - s=self.ffi.new("char**",self.ffi.NULL) - self.lib.dvdnav_get_title_string(self.dvd,s) - self.title=str(self.ffi.string(s[0]),"utf8").strip() or None - self.lib.dvdnav_get_serial_string(self.dvd,s) - self.serial=str(self.ffi.string(s[0]),"utf8").strip() or None + s = self.ffi.new("char**", self.ffi.NULL) + self.lib.dvdnav_get_title_string(self.dvd, s) + self.title = str(self.ffi.string(s[0]), "utf8").strip() or None + self.lib.dvdnav_get_serial_string(self.dvd, s) + self.serial = str(self.ffi.string(s[0]), "utf8").strip() or None self.__get_titles() - def open(self,path): - audio_attrs=self.ffi.new("audio_attr_t*") - spu_attr=self.ffi.new("subp_attr_t*") - dvdnav=self.ffi.new("dvdnav_t**",self.ffi.cast("dvdnav_t*",0)) - self.__check_error(self.lib.dvdnav_open(dvdnav,bytes(path,"utf8"))) - self.dvd=dvdnav[0] - self.__check_error(self.lib.dvdnav_set_readahead_flag(self.dvd,1)) + def open(self, path): + audio_attrs = self.ffi.new("audio_attr_t*") + spu_attr = self.ffi.new("subp_attr_t*") + dvdnav = self.ffi.new("dvdnav_t**", self.ffi.cast("dvdnav_t*", 0)) + self.__check_error(self.lib.dvdnav_open(dvdnav, bytes(path, "utf8"))) + self.dvd = dvdnav[0] + self.__check_error(self.lib.dvdnav_set_readahead_flag(self.dvd, 1)) self.__get_info() for title in self.titles: - self.__check_error(self.lib.dvdnav_title_play(self.dvd,title)) - self.titles[title]['audio']={} - self.titles[title]['subtitles']={} + self.__check_error(self.lib.dvdnav_title_play(self.dvd, title)) + self.titles[title]["audio"] = {} + self.titles[title]["subtitles"] = {} for n in range(255): - stream_id=self.lib.dvdnav_get_audio_logical_stream(self.dvd,n) - if stream_id==-1: + stream_id = self.lib.dvdnav_get_audio_logical_stream(self.dvd, n) + if stream_id == -1: continue - self.__check_error(self.lib.dvdnav_get_audio_attr(self.dvd,stream_id,audio_attrs)) - alang=None + self.__check_error( + self.lib.dvdnav_get_audio_attr(self.dvd, stream_id, audio_attrs) + ) + alang = None if audio_attrs.lang_type: - alang = str(audio_attrs.lang_code.to_bytes(2,'big'),"utf8") - channels = audio_attrs.channels+1 - codec = { - 0: 'ac3', - 2: 'mpeg1', - 3: 'mpeg-2ext', - 4: 'lpcm', - 6: 'dts' - }[audio_attrs.audio_format] - audio_type={ + alang = str(audio_attrs.lang_code.to_bytes(2, "big"), "utf8") + channels = audio_attrs.channels + 1 + codec = {0: "ac3", 2: "mpeg1", 3: "mpeg-2ext", 4: "lpcm", 6: "dts"}[ + audio_attrs.audio_format + ] + audio_type = { 0: None, - 1: 'normal', - 2: 'descriptive', + 1: "normal", + 2: "descriptive", 3: "director's commentary", - 4: "alternate director's commentary" + 4: "alternate director's commentary", }[audio_attrs.code_extension] - self.titles[title]['audio'][n]={ - 'stream_id': stream_id, - 'lang':alang, - 'channels': channels, - 'codec': codec, - 'type': audio_type + self.titles[title]["audio"][n] = { + "stream_id": stream_id, + "lang": alang, + "channels": channels, + "codec": codec, + "type": audio_type, } for n in range(255): - stream_id=self.lib.dvdnav_get_spu_logical_stream(self.dvd,n) - if stream_id==-1: + stream_id = self.lib.dvdnav_get_spu_logical_stream(self.dvd, n) + if stream_id == -1: continue - self.__check_error(self.lib.dvdnav_get_spu_attr(self.dvd,stream_id,spu_attr)) + self.__check_error( + self.lib.dvdnav_get_spu_attr(self.dvd, stream_id, spu_attr) + ) slang = None - if spu_attr.type==1: - slang = str(spu_attr.lang_code.to_bytes(2,'big'),"utf8") - self.titles[title]['subtitles'][n]={ - 'stream_id': stream_id, - 'lang':slang + if spu_attr.type == 1: + slang = str(spu_attr.lang_code.to_bytes(2, "big"), "utf8") + self.titles[title]["subtitles"][n] = { + "stream_id": stream_id, + "lang": slang, } - self.__check_error(self.lib.dvdnav_stop(self.dvd)) \ No newline at end of file + self.__check_error(self.lib.dvdnav_stop(self.dvd)) diff --git a/dvdread.py b/dvdread.py index a34e922..f76348d 100644 --- a/dvdread.py +++ b/dvdread.py @@ -4,16 +4,24 @@ import functools import binascii from datetime import timedelta -def loadlib(dll_path,*includes,**kwargs): + +def loadlib(dll_path, *includes, **kwargs): ffi = cffi.FFI() for include in includes: - ffi.cdef(open(include).read(),**kwargs) - return ffi,ffi.dlopen(dll_path) + ffi.cdef(open(include).read(), **kwargs) + return ffi, ffi.dlopen(dll_path) + class DVDRead(object): - def __init__(self,path): - self.dvd=None - self.ffi,self.lib = loadlib("libdvdread-8.dll", + def __init__(self, path, verbose="0", method="disc"): + if verbose is None: + os.environ.pop("DVDCSS_VERBOSE", None) + else: + os.environ["DVDCSS_VERBOSE"] = str(verbose) + os.environ["DVDCSS_METHOD"] = method + self.dvd = None + self.ffi, self.lib = loadlib( + "libdvdread-8.dll", "dvd_types.h", "dvd_reader.h", "ifo_types.h", @@ -21,23 +29,137 @@ class DVDRead(object): "ifo_print.h", "nav_types.h", "nav_read.h", - "nav_print.h") - self.path=path - self.titles={} + "nav_print.h", + packed=True, + ) + self.path = path + self.titles = {} self.open(path) + self.lb_len=self.lib.DVD_VIDEO_LB_LEN + + def grab_ifo(self,title,bup=False): + from tqdm import tqdm + buf = self.ffi.new("unsigned char[]", 512) + if bup: + fh = self.lib.DVDOpenFile(self.dvd, title, self.lib.DVD_READ_INFO_BACKUP_FILE) + else: + fh = self.lib.DVDOpenFile(self.dvd, title, self.lib.DVD_READ_INFO_FILE) + total_size = self.lib.DVDFileSize(fh)*self.lb_len + remaining = total_size + num_read = True + pbar = tqdm(total=total_size, unit="iB", unit_scale=True, unit_divisor=1024,leave=False) + while num_read: + num_read=self.lib.DVDReadBytes( fh, buf, 512) + num_read=min(num_read,remaining) + remaining-=num_read + pbar.update(num_read) + yield self.ffi.buffer(buf,num_read)[:] + self.lib.DVDCloseFile(fh) + + def grab_ifos(self): + vmg_ifo = self.lib.ifoOpen(self.dvd, 0) + if vmg_ifo == self.ffi.NULL: + return + title_sets = vmg_ifo.vts_atrt.nr_of_vtss + for t in range(1,title_sets + 1): + vts = self.lib.ifoOpen(self.dvd, t) + if vts == self.ffi.NULL: + continue + self.lib.ifoClose(vts) + outfile=os.path.join("RIP",f"VTS_{t:02}_0.ifo") + with open(outfile, "wb") as out_ifo: + for block in self.grab_ifo(t,bup=False): + out_ifo.write(block) + outfile=os.path.join("RIP",f"VTS_{t:02}_0.bup") + with open(outfile, "wb") as out_ifo: + for block in self.grab_ifo(t,bup=True): + out_ifo.write(block) + self.lib.ifoClose(vmg_ifo) + + def grab_vob(self,title): + from tqdm import tqdm + buf = self.ffi.new("unsigned char[]", 512 * self.lb_len) + fh = self.lib.DVDOpenFile(self.dvd, title, self.lib.DVD_READ_TITLE_VOBS) + total_size = self.lib.DVDFileSize(fh)*self.lb_len + remaining = total_size + num_read = True + pos=0 + pbar = tqdm(total=total_size, unit="iB", unit_scale=True, unit_divisor=1024,leave=False) + while remaining: + num_read=self.lib.DVDReadBlocks( fh,pos, 512, buf) + if num_read<0: + raise RuntimeError("Error reading!") + num_read_bytes=num_read*self.lb_len + num_read_bytes=min(num_read_bytes,remaining) + remaining-=num_read_bytes + pbar.update(num_read_bytes) + yield self.ffi.buffer(buf,num_read_bytes)[:] + pos+=num_read + pbar.close() + self.lib.DVDCloseFile(fh) + + def grab_vobs(self): + vmg_ifo = self.lib.ifoOpen(self.dvd, 0) + if vmg_ifo == self.ffi.NULL: + return + title_sets = vmg_ifo.vts_atrt.nr_of_vtss + for t in range(1,title_sets + 1): + vts = self.lib.ifoOpen(self.dvd, t) + if vts == self.ffi.NULL: + continue + self.lib.ifoClose(vts) + outfile=os.path.join("RIP",f"VTS_{t:02}_0.vob") + with open(outfile, "wb") as out_ifo: + for block in self.grab_vob(t): + out_ifo.write(block) + self.lib.ifoClose(vmg_ifo) + + + def test(self): + from tqdm import tqdm + fn = 0 + chunk_size = 2048 + buf = self.ffi.new("unsigned char[]", chunk_size * 2048) + for fn in range(title_sets + 1): + pos = 0 + fh = self.lib.DVDOpenFile(self.dvd, fn, self.lib.DVD_READ_TITLE_VOBS) + if fh: + total_size = self.lib.DVDFileSize(fh) + if total_size == -1: + self.lib.DVDCloseFile(fh) + break + pbar = tqdm(total=total_size * 2048, unit="iB", unit_scale=True, unit_divisor=1024,leave=False) + last=False + with open(f"out_{fn}.vob", "wb") as out_vob: + while True: + if (pos+chunk_size)>total_size: + chunk_size=total_size-pos + count = self.lib.DVDReadBlocks(fh, pos, chunk_size, buf) + if count == -1: + break + pbar.update( + out_vob.write(self.ffi.buffer(buf, count * 2048)[:]) + ) + pos += count + if pos>=total_size: + break + self.lib.DVDCloseFile(fh) + fn += 1 + if fn>200: + break def __del__(self): if self.dvd: self.lib.DVDClose(self.dvd) - def open(self,path): + def open(self, path): # self.dvd_css=self.css_lib.dvdcss_open() - self.dvd=self.lib.DVDOpen(bytes(path,"utf8")) - vol_id=self.ffi.new("unsigned char[]",32) - self.lib.DVDDiscID(self.dvd,vol_id) - self.disc_id=str(binascii.hexlify(self.ffi.buffer(vol_id,16)[:]),"utf8") - self.lib.DVDUDFVolumeInfo(self.dvd,vol_id,32,self.ffi.NULL,0) - self.udf_disc_name=str(self.ffi.string(vol_id),"utf8") - self.lib.DVDISOVolumeInfo(self.dvd,vol_id,32,self.ffi.NULL,0) - self.iso_disc_name=str(self.ffi.string(vol_id),"utf8") - self.ffi.release(vol_id) \ No newline at end of file + self.dvd = self.lib.DVDOpen(bytes(path, "utf8")) + vol_id = self.ffi.new("unsigned char[]", 32) + self.lib.DVDDiscID(self.dvd, vol_id) + self.disc_id = str(binascii.hexlify(self.ffi.buffer(vol_id, 16)[:]), "utf8") + self.lib.DVDUDFVolumeInfo(self.dvd, vol_id, 32, self.ffi.NULL, 0) + self.udf_disc_name = str(self.ffi.string(vol_id), "utf8") + self.lib.DVDISOVolumeInfo(self.dvd, vol_id, 32, self.ffi.NULL, 0) + self.iso_disc_name = str(self.ffi.string(vol_id), "utf8") + self.ffi.release(vol_id) diff --git a/ff_d2v.py b/ff_d2v.py index 2262fb3..55e9c7e 100644 --- a/ff_d2v.py +++ b/ff_d2v.py @@ -6,7 +6,7 @@ import itertools as ITT from tqdm import tqdm -colorspace={ +colorspace = { "gbr": 0, "bt709": 1, "unknown": 2, @@ -23,151 +23,183 @@ colorspace={ "ictcp": 14, } -pict_types={ - 'I':0b01, - 'P':0b10, - 'B':0b11 -} +pict_types = {"I": 0b01, "P": 0b10, "B": 0b11} + def make_info(frames): - has_interlaced = any(frame['interlaced_frame'] for frame in frames) - new_gop='timecode' in frames[0].get('tags',{}) - info=0x000 - info|=1<<11 # always 1 - info|=0<<10 # 0=Closed GOP, 1=Open GOP - info|=(not has_interlaced)<<9 # Progressive - info|=new_gop<<8 + has_interlaced = any(frame["interlaced_frame"] for frame in frames) + new_gop = "timecode" in frames[0].get("tags", {}) + info = 0x000 + info |= 1 << 11 # always 1 + info |= 0 << 10 # 0=Closed GOP, 1=Open GOP + info |= (not has_interlaced) << 9 # Progressive + info |= new_gop << 8 return info + def make_flags(frames): - flags=[] + flags = [] for frame in frames: - needs_prev=False - progressive=not int(frame['interlaced_frame']) - pt=pict_types[frame['pict_type']] - reserved=0b00 - tff=int(frame['top_field_first']) - rff=int(frame['repeat_pict']) - flag=0b0 - flag|=(not needs_prev)<<7 - flag|=progressive<<6 - flag|=pt<<4 - flag|=reserved<<2 - flag|=tff<<1 - flag|=rff + needs_prev = False + progressive = not int(frame["interlaced_frame"]) + pt = pict_types[frame["pict_type"]] + reserved = 0b00 + tff = int(frame["top_field_first"]) + rff = int(frame["repeat_pict"]) + flag = 0b0 + flag |= (not needs_prev) << 7 + flag |= progressive << 6 + flag |= pt << 4 + flag |= reserved << 2 + flag |= tff << 1 + flag |= rff flags.append(f"{flag:02x}") return flags -def make_line(frames,stream): - info=f"{make_info(frames):03x}" - matrix=colorspace[stream['color_space']] - file=0 - position=frames[0]['pkt_pos'] - skip=0 - vob=0 - cell=0 - flags=make_flags(frames) - return " ".join(map(str,[info,matrix,file,position,skip,vob,cell,*flags])) +def make_line(frames, stream): + info = f"{make_info(frames):03x}" + matrix = colorspace[stream["color_space"]] + file = 0 + position = frames[0]["pkt_pos"] + skip = 0 + vob = 0 + cell = 0 + flags = make_flags(frames) + return " ".join(map(str, [info, matrix, file, position, skip, vob, cell, *flags])) def get_frames(path): - proc=SP.Popen([ - "ffprobe", - "-probesize", str(0x7fffffff), - "-analyzeduration", str(0x7fffffff), - "-v","fatal", - "-i",path, - "-select_streams","v:0", - "-show_frames", - "-print_format","compact" - ],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0) - data=None + proc = SP.Popen( + [ + "ffprobe", + "-probesize", + str(0x7FFFFFFF), + "-analyzeduration", + str(0x7FFFFFFF), + "-v", + "fatal", + "-i", + path, + "-select_streams", + "v:0", + "-show_frames", + "-print_format", + "compact", + ], + stdout=SP.PIPE, + stdin=SP.DEVNULL, + bufsize=0, + ) + data = None for line in proc.stdout: - line=str(line,"utf8").strip().split("|") - line={line[0]: dict(v.split("=") for v in line[1:])} + line = str(line, "utf8").strip().split("|") + line = {line[0]: dict(v.split("=") for v in line[1:])} yield line - ret=proc.wait() - if ret!=0: + ret = proc.wait() + if ret != 0: exit(ret) return data + def get_streams(path): - proc=SP.Popen([ - "ffprobe", - "-probesize", str(0x7fffffff), - "-analyzeduration", str(0x7fffffff), - "-v","fatal", - "-i",path, - "-select_streams","v:0", - "-show_streams", - "-show_format", - "-print_format","json" - ],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0) - data=json.load(proc.stdout) - ret=proc.wait() - if ret!=0: + proc = SP.Popen( + [ + "ffprobe", + "-probesize", + str(0x7FFFFFFF), + "-analyzeduration", + str(0x7FFFFFFF), + "-v", + "fatal", + "-i", + path, + "-select_streams", + "v:0", + "-show_streams", + "-show_format", + "-print_format", + "json", + ], + stdout=SP.PIPE, + stdin=SP.DEVNULL, + bufsize=0, + ) + data = json.load(proc.stdout) + ret = proc.wait() + if ret != 0: exit(ret) - return data['streams'],data['format'] + return data["streams"], data["format"] + def make_header(file): - return ["DGIndexProjectFile16","1",os.path.abspath(file)] + return ["DGIndexProjectFile16", "1", os.path.abspath(file)] + def make_settings(stream): - pict_size="x".join(map(str,[stream["width"],stream["height"]])) - frame_rate = list(map(int,stream['r_frame_rate'].split("/"))) - frame_rate=(frame_rate[0]*1000)//frame_rate[1] - frame_rate=f"{frame_rate} ({stream['r_frame_rate']})" - header=[ - ("Stream_Type",0), # Elementary Stream - ("MPEG_Type",2), # MPEG-2 - ("iDCT_Algorithm",5), # 64-bit IEEE-1180 Reference - ("YUVRGB_Scale",int(stream["color_range"]!="tv")), - ("Luminance_Filter","0,0"), - ("Clipping","0,0,0,0"), - ("Aspect_Ratio",stream["display_aspect_ratio"]), - ("Picture_Size",pict_size), - ("Field_Operation",0), # Honor Pulldown Flags + pict_size = "x".join(map(str, [stream["width"], stream["height"]])) + frame_rate = list(map(int, stream["r_frame_rate"].split("/"))) + frame_rate = (frame_rate[0] * 1000) // frame_rate[1] + frame_rate = f"{frame_rate} ({stream['r_frame_rate']})" + header = [ + ("Stream_Type", 0), # Elementary Stream + ("MPEG_Type", 2), # MPEG-2 + ("iDCT_Algorithm", 5), # 64-bit IEEE-1180 Reference + ("YUVRGB_Scale", int(stream["color_range"] != "tv")), + ("Luminance_Filter", "0,0"), + ("Clipping", "0,0,0,0"), + ("Aspect_Ratio", stream["display_aspect_ratio"]), + ("Picture_Size", pict_size), + ("Field_Operation", 0), # Honor Pulldown Flags ("Frame_Rate", frame_rate), - ("Location",f"0,0,0,0"), + ("Location", "0,0,0,0"), ] - for k,v in header: + for k, v in header: yield f"{k}={v}" + def gen_d2v(path): yield from make_header(path) yield "" - streams,fmt=get_streams(path) - stream=[s for s in streams if s['codec_type']=='video'][0] - stream['index']=str(stream['index']) + streams, fmt = get_streams(path) + stream = [s for s in streams if s["codec_type"] == "video"][0] + stream["index"] = str(stream["index"]) yield from make_settings(stream) yield "" - line_buffer=[] - frames=get_frames(path) - prog_bar=tqdm(frames,total=int(fmt['size']),unit_divisor=1024,unit_scale=True,unit="iB",dec="Writing d2v") + line_buffer = [] + frames = get_frames(path) + prog_bar = tqdm( + frames, + total=int(fmt["size"]), + unit_divisor=1024, + unit_scale=True, + unit="iB", + desc="Writing d2v", + ) for line in prog_bar: - if 'frame' not in line: + if "frame" not in line: continue - frame=line['frame'] - prog_bar.n=min(max(prog_bar.n,int(frame['pkt_pos'])),int(fmt['size'])) + frame = line["frame"] + prog_bar.n = min(max(prog_bar.n, int(frame["pkt_pos"])), int(fmt["size"])) prog_bar.update(0) - if frame['stream_index']!=stream['index']: + if frame["stream_index"] != stream["index"]: continue - if frame['pict_type']=="I" and line_buffer: - yield make_line(line_buffer,stream) + if frame["pict_type"] == "I" and line_buffer: + yield make_line(line_buffer, stream) line_buffer.clear() line_buffer.append(frame) prog_bar.close() yield None + def make_d2v(path): - outfile=os.path.splitext(os.path.basename(path))[0] - outfile=os.path.extsep.join([outfile,"d2v"]) - a,b=ITT.tee(gen_d2v(path)) + outfile = os.path.splitext(os.path.basename(path))[0] + outfile = os.path.extsep.join([outfile, "d2v"]) + a, b = ITT.tee(gen_d2v(path)) next(b) - with open(outfile,"w") as fh: - for line,next_line in zip(a,b): + with open(outfile, "w") as fh: + for line, next_line in zip(a, b): fh.write(line) - if next_line is None: # last line, append end marker + if next_line is None: # last line, append end marker fh.write(" ff") fh.write("\n") diff --git a/vob_demux.py b/vob_demux.py index eec2025..b3d0d33 100644 --- a/vob_demux.py +++ b/vob_demux.py @@ -5,59 +5,83 @@ import subprocess as SP def get_streams(path): - proc=SP.Popen([ - "ffprobe", - "-probesize", str(0x7fffffff), - "-analyzeduration", str(0x7fffffff), - "-v","fatal", - "-i",path, - "-show_streams", - "-show_format", - "-print_format","json" - ],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0) - data=json.load(proc.stdout) - ret=proc.wait() - if ret!=0: - return [],{} - return data['streams'],data['format'] + proc = SP.Popen( + [ + "ffprobe", + "-probesize", + str(0x7FFFFFFF), + "-analyzeduration", + str(0x7FFFFFFF), + "-v", + "fatal", + "-i", + path, + "-show_streams", + "-show_format", + "-print_format", + "json", + ], + stdout=SP.PIPE, + stdin=SP.DEVNULL, + bufsize=0, + ) + data = json.load(proc.stdout) + ret = proc.wait() + if ret != 0: + return [], {} + return data["streams"], data["format"] -types={ - 'mpeg2video': 'm2v', - 'ac3': 'ac3', - 'dvd_subtitle': 'sup', + +types = { + "mpeg2video": "m2v", + "ac3": "ac3", + "dvd_subtitle": "sup", } + def demux(path): - folder=os.path.dirname(path) - basename=os.path.splitext(os.path.basename(path))[0] - streams,fmt=get_streams(path) - cmd=[ + folder = os.path.dirname(path) + basename = os.path.splitext(os.path.basename(path))[0] + streams, fmt = get_streams(path) + cmd = [ "ffmpeg", "-y", - "-strict","-2", - "-fflags","+genpts", - "-probesize", str(0x7fffffff), - "-analyzeduration", str(0x7fffffff), - "-i",path, - "-scodec","copy", - "-vcodec","copy", - "-acodec","copy", + # "-fflags","+genpts+igndts", + "-probesize", + str(0x7FFFFFFF), + "-analyzeduration", + str(0x7FFFFFFF), + "-i", + path, + "-strict", + "-2", + "-vcodec", + "copy", + "-acodec", + "copy", + "-scodec", + "copy", ] - need_ffmpeg=False + need_ffmpeg = False for stream in streams: - codec=stream['codec_name'] - ext=types.get(codec,codec) - idx=stream['index'] - hex_id=stream['id'] - codec_name=stream['codec_long_name'] - outfile=os.path.join(folder,f"{basename}_{idx}_{hex_id}") - if codec=="dvd_subtitle": + codec = stream["codec_name"] + ext = types.get(codec, codec) + idx = stream["index"] + hex_id = stream["id"] + codec_name = stream["codec_long_name"] + outfile = os.path.join(folder, f"{basename}_{idx}_{hex_id}") + if codec=="dvd_nav_packet": + continue + print(idx, hex_id, codec_name, codec) + if codec == "dvd_subtitle": SP.check_call([ "mencoder",path,"-vobsuboutindex",str(idx),"-vobsubout", outfile,"-nosound","-ovc", "copy", "-o",os.devnull ]) continue - print(idx,hex_id,codec_name,codec) - cmd+=["-map",f"0:#{hex_id}",outfile+f".{ext}"] - need_ffmpeg=True + cmd += ["-map", f"0:#{hex_id}", "-strict", "-2", outfile + f".{ext}"] + need_ffmpeg = True if need_ffmpeg: - SP.check_call(cmd) \ No newline at end of file + SP.check_call(cmd) + +if __name__=="__main__": + demux(sys.argv[1]) \ No newline at end of file