212 lines
9.3 KiB
Python
212 lines
9.3 KiB
Python
|
import cffi
|
||
|
import os
|
||
|
import functools
|
||
|
from datetime import timedelta
|
||
|
from tqdm import tqdm
|
||
|
|
||
|
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)
|
||
|
|
||
|
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",
|
||
|
"dvd_types.h",
|
||
|
"dvd_reader.h",
|
||
|
"ifo_types.h",
|
||
|
"nav_types.h",
|
||
|
"dvdnav_events.h",
|
||
|
"dvdnav.h")
|
||
|
self.path=path
|
||
|
self.titles={}
|
||
|
self.open(path)
|
||
|
|
||
|
def __del__(self):
|
||
|
self.__check_error(self.lib.dvdnav_close(self.dvd))
|
||
|
self.dvd=None
|
||
|
|
||
|
def __repr__(self):
|
||
|
return "<DVD Path={0.path} Title={0.title} Serial={0.serial}".format(self)
|
||
|
|
||
|
def get_blocks(self,title,angle=1,slang=None):
|
||
|
self.__check_error(self.lib.dvdnav_set_PGC_positioning_flag(self.dvd,1))
|
||
|
self.__check_error(self.lib.dvdnav_title_play(self.dvd,title))
|
||
|
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)
|
||
|
domains = {
|
||
|
1: "FirstPlay",
|
||
|
2: "VTSTitle",
|
||
|
4: "VMGM",
|
||
|
8: "VTSMenu"
|
||
|
}
|
||
|
events={
|
||
|
0: "DVDNAV_BLOCK_OK",
|
||
|
1: "DVDNAV_NOP",
|
||
|
2: "DVDNAV_STILL_FRAME",
|
||
|
3: "DVDNAV_SPU_STREAM_CHANGE",
|
||
|
4: "DVDNAV_AUDIO_STREAM_CHANGE",
|
||
|
5: "DVDNAV_VTS_CHANGE",
|
||
|
6: "DVDNAV_CELL_CHANGE",
|
||
|
7: "DVDNAV_NAV_PACKET",
|
||
|
8: "DVDNAV_STOP",
|
||
|
9: "DVDNAV_HIGHLIGHT",
|
||
|
10: "DVDNAV_SPU_CLUT_CHANGE",
|
||
|
12: "DVDNAV_HOP_CHANNEL",
|
||
|
13: "DVDNAV_WAIT"
|
||
|
}
|
||
|
progbar=tqdm(unit_divisor=1024,unit_scale=True,unit="iB",desc="Ripping DVD")
|
||
|
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))
|
||
|
progbar.update(0)
|
||
|
# 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]:
|
||
|
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:
|
||
|
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")
|
||
|
self.__check_error(self.lib.dvdnav_still_skip(self.dvd))
|
||
|
elif ev[0]==self.lib.DVDNAV_WAIT:
|
||
|
# print("Wait",size[0])
|
||
|
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
|
||
|
break
|
||
|
else:
|
||
|
progbar.write(f"[{title}|{angle}] Unhandled: {events.get(ev[0],ev[0])} {size[0]}")
|
||
|
|
||
|
|
||
|
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))
|
||
|
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:
|
||
|
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:
|
||
|
continue
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
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))
|
||
|
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']={}
|
||
|
for n in range(255):
|
||
|
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
|
||
|
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={
|
||
|
0: None,
|
||
|
1: 'normal',
|
||
|
2: 'descriptive',
|
||
|
3: "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
|
||
|
}
|
||
|
for n in range(255):
|
||
|
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))
|
||
|
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
|
||
|
}
|
||
|
self.__check_error(self.lib.dvdnav_stop(self.dvd))
|