Push latest changes
This commit is contained in:
parent
be4930a8a3
commit
aeb624bcf3
5 changed files with 578 additions and 320 deletions
|
@ -2,37 +2,64 @@ import cffi
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from dvdnav import DVDNav
|
from dvdnav import DVDNav,DVDError
|
||||||
from dvdread import DVDRead
|
from dvdread import DVDRead
|
||||||
import subprocess as SP
|
import subprocess as SP
|
||||||
import json
|
import json
|
||||||
|
from glob import glob
|
||||||
|
import itertools as ITT
|
||||||
from vob_demux import demux
|
from vob_demux import demux
|
||||||
from ff_d2v import make_d2v
|
from ff_d2v import make_d2v
|
||||||
|
|
||||||
def loadlib(dll_path, *includes, **kwargs):
|
def loadlib(dll_path, *includes, **kwargs):
|
||||||
ffi = cffi.FFI()
|
ffi = cffi.FFI()
|
||||||
for include in includes:
|
for include in includes:
|
||||||
ffi.cdef(open(include).read(), kwargs)
|
ffi.cdef(open(include).read(), kwargs)
|
||||||
return ffi, ffi.dlopen(dll_path)
|
return ffi, ffi.dlopen(dll_path)
|
||||||
|
|
||||||
os.environ["DVDCSS_VERBOSE"]="2"
|
for dvd_path in ITT.chain.from_iterable(map(glob,sys.argv[1:])):
|
||||||
os.environ["DVDCSS_METHOD"]="disc"
|
r = DVDRead(dvd_path)
|
||||||
|
# r.grab_ifos()
|
||||||
|
# r.grab_vobs()
|
||||||
|
# exit()
|
||||||
|
|
||||||
r=DVDRead(sys.argv[1])
|
out_folder = os.path.join(
|
||||||
out_folder=os.path.join("out","_".join([r.disc_id,r.udf_disc_name or r.iso_disc_name]).replace(" ","_"))
|
"out", "_".join([r.disc_id, r.udf_disc_name or r.iso_disc_name]).replace(" ", "_")
|
||||||
del r
|
)
|
||||||
os.makedirs(out_folder, exist_ok=True)
|
os.makedirs(out_folder, exist_ok=True)
|
||||||
d=DVDNav(sys.argv[1])
|
d = DVDNav(dvd_path)
|
||||||
|
to_demux = []
|
||||||
for k, v in d.titles.items():
|
for k, v in d.titles.items():
|
||||||
v['duration']=v['duration'].total_seconds()
|
v["duration"] = v["duration"].total_seconds()
|
||||||
v['chapters']=[c.total_seconds() for c in v['chapters']]
|
v["chapters"] = [c.total_seconds() for c in v["chapters"]]
|
||||||
d.titles[k] = v
|
d.titles[k] = v
|
||||||
with open(os.path.join(out_folder,f"{k}.json"),"w") as fh:
|
with open(os.path.join(out_folder, f"{k:03}.json"), "w") as fh:
|
||||||
json.dump(v, fh)
|
json.dump(v, fh)
|
||||||
for a in range(v['angles']):
|
for a in range(0,99):
|
||||||
a+=1
|
block=0
|
||||||
outfile=os.path.join(out_folder,f"{k}_{a}.vob")
|
outfile = os.path.join(out_folder, f"t{k:03}_a{a:03}_b{block:03}.vob")
|
||||||
with open(outfile,"wb") as fh:
|
to_demux.append(outfile)
|
||||||
|
fh = open(outfile, "wb")
|
||||||
|
try:
|
||||||
for block in d.get_blocks(k, a):
|
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)
|
fh.write(block)
|
||||||
demux(outfile)
|
except DVDError as e:
|
||||||
os.unlink(outfile)
|
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)
|
||||||
|
|
159
dvdnav.py
159
dvdnav.py
|
@ -3,6 +3,8 @@ import os
|
||||||
import functools
|
import functools
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
from dvdread import DVDRead
|
||||||
|
|
||||||
|
|
||||||
def loadlib(dll_path, *includes, **kwargs):
|
def loadlib(dll_path, *includes, **kwargs):
|
||||||
ffi = cffi.FFI()
|
ffi = cffi.FFI()
|
||||||
|
@ -10,21 +12,29 @@ def loadlib(dll_path,*includes,**kwargs):
|
||||||
ffi.cdef(open(include).read(), kwargs)
|
ffi.cdef(open(include).read(), kwargs)
|
||||||
return ffi, ffi.dlopen(dll_path)
|
return ffi, ffi.dlopen(dll_path)
|
||||||
|
|
||||||
|
|
||||||
class DVDError(Exception):
|
class DVDError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DVDNav(object):
|
class DVDNav(object):
|
||||||
def __init__(self,path,verbose=2,method="disc"):
|
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_VERBOSE"] = str(verbose)
|
||||||
os.environ["DVDCSS_METHOD"] = method
|
os.environ["DVDCSS_METHOD"] = method
|
||||||
self.dvd = None
|
self.dvd = None
|
||||||
self.ffi,self.lib = loadlib("libdvdnav-4.dll",
|
self.ffi, self.lib = loadlib(
|
||||||
|
"libdvdnav-4.dll",
|
||||||
"dvd_types.h",
|
"dvd_types.h",
|
||||||
"dvd_reader.h",
|
"dvd_reader.h",
|
||||||
"ifo_types.h",
|
"ifo_types.h",
|
||||||
"nav_types.h",
|
"nav_types.h",
|
||||||
"dvdnav_events.h",
|
"dvdnav_events.h",
|
||||||
"dvdnav.h")
|
"dvdnav.h",
|
||||||
|
pack=True,
|
||||||
|
)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.titles = {}
|
self.titles = {}
|
||||||
self.open(path)
|
self.open(path)
|
||||||
|
@ -39,6 +49,15 @@ class DVDNav(object):
|
||||||
def get_blocks(self, title, angle=1, slang=None):
|
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_set_PGC_positioning_flag(self.dvd, 1))
|
||||||
self.__check_error(self.lib.dvdnav_title_play(self.dvd, title))
|
self.__check_error(self.lib.dvdnav_title_play(self.dvd, title))
|
||||||
|
curr_angle = self.ffi.new("int32_t*", 0)
|
||||||
|
num_angles = self.ffi.new("int32_t*", 0)
|
||||||
|
self.__check_error(
|
||||||
|
self.lib.dvdnav_get_angle_info(self.dvd, curr_angle, num_angles)
|
||||||
|
)
|
||||||
|
if angle != 0:
|
||||||
|
if angle < 1 or angle > num_angles[0]:
|
||||||
|
raise DVDError("Invalid angle specified!")
|
||||||
|
if angle != curr_angle[0]:
|
||||||
self.__check_error(self.lib.dvdnav_angle_change(self.dvd, angle))
|
self.__check_error(self.lib.dvdnav_angle_change(self.dvd, angle))
|
||||||
if slang is not None:
|
if slang is not None:
|
||||||
self.__check_error(self.lib.dvdnav_spu_language_select(self.dvd, slang))
|
self.__check_error(self.lib.dvdnav_spu_language_select(self.dvd, slang))
|
||||||
|
@ -52,7 +71,7 @@ class DVDNav(object):
|
||||||
1: "FirstPlay",
|
1: "FirstPlay",
|
||||||
2: "VTSTitle",
|
2: "VTSTitle",
|
||||||
4: "VMGM",
|
4: "VMGM",
|
||||||
8: "VTSMenu"
|
8: "VTSMenu",
|
||||||
}
|
}
|
||||||
events = {
|
events = {
|
||||||
0: "DVDNAV_BLOCK_OK",
|
0: "DVDNAV_BLOCK_OK",
|
||||||
|
@ -67,51 +86,83 @@ class DVDNav(object):
|
||||||
9: "DVDNAV_HIGHLIGHT",
|
9: "DVDNAV_HIGHLIGHT",
|
||||||
10: "DVDNAV_SPU_CLUT_CHANGE",
|
10: "DVDNAV_SPU_CLUT_CHANGE",
|
||||||
12: "DVDNAV_HOP_CHANNEL",
|
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:
|
while True:
|
||||||
self.__check_error(self.lib.dvdnav_get_next_block(self.dvd, buf, ev, size))
|
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:
|
if (
|
||||||
|
self.lib.dvdnav_get_position(self.dvd, pos, total_size)
|
||||||
|
== self.lib.DVDNAV_STATUS_OK
|
||||||
|
):
|
||||||
progbar.total = total_size[0] * 2048
|
progbar.total = total_size[0] * 2048
|
||||||
progbar.n = max(progbar.n, min(progbar.total, pos[0] * 2048))
|
progbar.n = max(progbar.n, min(progbar.total, pos[0] * 2048))
|
||||||
progbar.update(0)
|
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])
|
# 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
|
continue
|
||||||
elif ev[0] == self.lib.DVDNAV_BLOCK_OK:
|
elif ev[0] == self.lib.DVDNAV_BLOCK_OK:
|
||||||
# print("Read",size[0])
|
|
||||||
yield self.ffi.buffer(buf, size[0])[:]
|
yield self.ffi.buffer(buf, size[0])[:]
|
||||||
elif ev[0] == self.lib.DVDNAV_STOP:
|
elif ev[0] == self.lib.DVDNAV_STOP:
|
||||||
|
progbar.write(f"[{title}|{angle}] Stop")
|
||||||
break
|
break
|
||||||
elif ev[0] == self.lib.DVDNAV_NAV_PACKET:
|
elif ev[0] == self.lib.DVDNAV_NAV_PACKET:
|
||||||
nav=self.lib.dvdnav_get_current_nav_pci(self.dvd)
|
pass
|
||||||
# print("PTS:",timedelta(seconds=nav.pci_gi.vobu_s_ptm/90000))
|
|
||||||
elif ev[0] == self.lib.DVDNAV_STILL_FRAME:
|
elif ev[0] == self.lib.DVDNAV_STILL_FRAME:
|
||||||
# print("Still")
|
|
||||||
self.__check_error(self.lib.dvdnav_still_skip(self.dvd))
|
self.__check_error(self.lib.dvdnav_still_skip(self.dvd))
|
||||||
elif ev[0] == self.lib.DVDNAV_WAIT:
|
elif ev[0] == self.lib.DVDNAV_WAIT:
|
||||||
# print("Wait",size[0])
|
|
||||||
self.__check_error(self.lib.dvdnav_wait_skip(self.dvd))
|
self.__check_error(self.lib.dvdnav_wait_skip(self.dvd))
|
||||||
elif ev[0] == self.lib.DVDNAV_SPU_STREAM_CHANGE:
|
elif ev[0] == self.lib.DVDNAV_SPU_STREAM_CHANGE:
|
||||||
spu=self.ffi.cast("dvdnav_spu_stream_change_event_t*",buf)
|
pass
|
||||||
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:
|
elif ev[0] == self.lib.DVDNAV_AUDIO_STREAM_CHANGE:
|
||||||
audio = self.ffi.cast("dvdnav_audio_stream_change_event_t*", buf)
|
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:
|
elif ev[0] == self.lib.DVDNAV_CELL_CHANGE:
|
||||||
cell = self.ffi.cast("dvdnav_cell_change_event_t*", buf)
|
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})")
|
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:
|
elif ev[0] == self.lib.DVDNAV_VTS_CHANGE:
|
||||||
vts = self.ffi.cast("dvdnav_vts_change_event_t*", buf)
|
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_vts = (vts.new_vtsN, vts.new_domain)
|
||||||
new_domain=sorted(domains[k] for k in domains if vts.new_domain&k)
|
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})")
|
# 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
|
if new_vts in ripped: # looped
|
||||||
|
progbar.write(f"[{title}|{angle}] Looped!")
|
||||||
break
|
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:
|
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):
|
def __check_error(self, ret):
|
||||||
if ret == self.lib.DVDNAV_STATUS_ERR:
|
if ret == self.lib.DVDNAV_STATUS_ERR:
|
||||||
|
@ -134,17 +185,19 @@ class DVDNav(object):
|
||||||
num_parts = titles[0]
|
num_parts = titles[0]
|
||||||
self.lib.dvdnav_get_number_of_angles(self.dvd, title, titles)
|
self.lib.dvdnav_get_number_of_angles(self.dvd, title, titles)
|
||||||
num_angles = titles[0]
|
num_angles = titles[0]
|
||||||
num_chapters=self.lib.dvdnav_describe_title_chapters(self.dvd,title,times,duration)
|
num_chapters = self.lib.dvdnav_describe_title_chapters(
|
||||||
|
self.dvd, title, times, duration
|
||||||
|
)
|
||||||
if duration[0] == 0:
|
if duration[0] == 0:
|
||||||
continue
|
continue
|
||||||
chapters = []
|
chapters = []
|
||||||
for t in range(num_chapters):
|
for t in range(num_chapters):
|
||||||
chapters.append(timedelta(seconds=times[0][t] / 90000))
|
chapters.append(timedelta(seconds=times[0][t] / 90000))
|
||||||
self.titles[title] = {
|
self.titles[title] = {
|
||||||
'parts':num_parts,
|
"parts": num_parts,
|
||||||
'angles': num_angles,
|
"angles": num_angles,
|
||||||
'duration': timedelta(seconds=duration[0]/90000),
|
"duration": timedelta(seconds=duration[0] / 90000),
|
||||||
'chapters': chapters
|
"chapters": chapters,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __get_info(self):
|
def __get_info(self):
|
||||||
|
@ -165,48 +218,48 @@ class DVDNav(object):
|
||||||
self.__get_info()
|
self.__get_info()
|
||||||
for title in self.titles:
|
for title in self.titles:
|
||||||
self.__check_error(self.lib.dvdnav_title_play(self.dvd, title))
|
self.__check_error(self.lib.dvdnav_title_play(self.dvd, title))
|
||||||
self.titles[title]['audio']={}
|
self.titles[title]["audio"] = {}
|
||||||
self.titles[title]['subtitles']={}
|
self.titles[title]["subtitles"] = {}
|
||||||
for n in range(255):
|
for n in range(255):
|
||||||
stream_id = self.lib.dvdnav_get_audio_logical_stream(self.dvd, n)
|
stream_id = self.lib.dvdnav_get_audio_logical_stream(self.dvd, n)
|
||||||
if stream_id == -1:
|
if stream_id == -1:
|
||||||
continue
|
continue
|
||||||
self.__check_error(self.lib.dvdnav_get_audio_attr(self.dvd,stream_id,audio_attrs))
|
self.__check_error(
|
||||||
|
self.lib.dvdnav_get_audio_attr(self.dvd, stream_id, audio_attrs)
|
||||||
|
)
|
||||||
alang = None
|
alang = None
|
||||||
if audio_attrs.lang_type:
|
if audio_attrs.lang_type:
|
||||||
alang = str(audio_attrs.lang_code.to_bytes(2,'big'),"utf8")
|
alang = str(audio_attrs.lang_code.to_bytes(2, "big"), "utf8")
|
||||||
channels = audio_attrs.channels + 1
|
channels = audio_attrs.channels + 1
|
||||||
codec = {
|
codec = {0: "ac3", 2: "mpeg1", 3: "mpeg-2ext", 4: "lpcm", 6: "dts"}[
|
||||||
0: 'ac3',
|
audio_attrs.audio_format
|
||||||
2: 'mpeg1',
|
]
|
||||||
3: 'mpeg-2ext',
|
|
||||||
4: 'lpcm',
|
|
||||||
6: 'dts'
|
|
||||||
}[audio_attrs.audio_format]
|
|
||||||
audio_type = {
|
audio_type = {
|
||||||
0: None,
|
0: None,
|
||||||
1: 'normal',
|
1: "normal",
|
||||||
2: 'descriptive',
|
2: "descriptive",
|
||||||
3: "director's commentary",
|
3: "director's commentary",
|
||||||
4: "alternate director's commentary"
|
4: "alternate director's commentary",
|
||||||
}[audio_attrs.code_extension]
|
}[audio_attrs.code_extension]
|
||||||
self.titles[title]['audio'][n]={
|
self.titles[title]["audio"][n] = {
|
||||||
'stream_id': stream_id,
|
"stream_id": stream_id,
|
||||||
'lang':alang,
|
"lang": alang,
|
||||||
'channels': channels,
|
"channels": channels,
|
||||||
'codec': codec,
|
"codec": codec,
|
||||||
'type': audio_type
|
"type": audio_type,
|
||||||
}
|
}
|
||||||
for n in range(255):
|
for n in range(255):
|
||||||
stream_id = self.lib.dvdnav_get_spu_logical_stream(self.dvd, n)
|
stream_id = self.lib.dvdnav_get_spu_logical_stream(self.dvd, n)
|
||||||
if stream_id == -1:
|
if stream_id == -1:
|
||||||
continue
|
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
|
slang = None
|
||||||
if spu_attr.type == 1:
|
if spu_attr.type == 1:
|
||||||
slang = str(spu_attr.lang_code.to_bytes(2,'big'),"utf8")
|
slang = str(spu_attr.lang_code.to_bytes(2, "big"), "utf8")
|
||||||
self.titles[title]['subtitles'][n]={
|
self.titles[title]["subtitles"][n] = {
|
||||||
'stream_id': stream_id,
|
"stream_id": stream_id,
|
||||||
'lang':slang
|
"lang": slang,
|
||||||
}
|
}
|
||||||
self.__check_error(self.lib.dvdnav_stop(self.dvd))
|
self.__check_error(self.lib.dvdnav_stop(self.dvd))
|
128
dvdread.py
128
dvdread.py
|
@ -4,16 +4,24 @@ import functools
|
||||||
import binascii
|
import binascii
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
def loadlib(dll_path, *includes, **kwargs):
|
def loadlib(dll_path, *includes, **kwargs):
|
||||||
ffi = cffi.FFI()
|
ffi = cffi.FFI()
|
||||||
for include in includes:
|
for include in includes:
|
||||||
ffi.cdef(open(include).read(), **kwargs)
|
ffi.cdef(open(include).read(), **kwargs)
|
||||||
return ffi, ffi.dlopen(dll_path)
|
return ffi, ffi.dlopen(dll_path)
|
||||||
|
|
||||||
|
|
||||||
class DVDRead(object):
|
class DVDRead(object):
|
||||||
def __init__(self,path):
|
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.dvd = None
|
||||||
self.ffi,self.lib = loadlib("libdvdread-8.dll",
|
self.ffi, self.lib = loadlib(
|
||||||
|
"libdvdread-8.dll",
|
||||||
"dvd_types.h",
|
"dvd_types.h",
|
||||||
"dvd_reader.h",
|
"dvd_reader.h",
|
||||||
"ifo_types.h",
|
"ifo_types.h",
|
||||||
|
@ -21,10 +29,124 @@ class DVDRead(object):
|
||||||
"ifo_print.h",
|
"ifo_print.h",
|
||||||
"nav_types.h",
|
"nav_types.h",
|
||||||
"nav_read.h",
|
"nav_read.h",
|
||||||
"nav_print.h")
|
"nav_print.h",
|
||||||
|
packed=True,
|
||||||
|
)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.titles = {}
|
self.titles = {}
|
||||||
self.open(path)
|
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):
|
def __del__(self):
|
||||||
if self.dvd:
|
if self.dvd:
|
||||||
|
|
112
ff_d2v.py
112
ff_d2v.py
|
@ -23,15 +23,12 @@ colorspace={
|
||||||
"ictcp": 14,
|
"ictcp": 14,
|
||||||
}
|
}
|
||||||
|
|
||||||
pict_types={
|
pict_types = {"I": 0b01, "P": 0b10, "B": 0b11}
|
||||||
'I':0b01,
|
|
||||||
'P':0b10,
|
|
||||||
'B':0b11
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_info(frames):
|
def make_info(frames):
|
||||||
has_interlaced = any(frame['interlaced_frame'] for frame in frames)
|
has_interlaced = any(frame["interlaced_frame"] for frame in frames)
|
||||||
new_gop='timecode' in frames[0].get('tags',{})
|
new_gop = "timecode" in frames[0].get("tags", {})
|
||||||
info = 0x000
|
info = 0x000
|
||||||
info |= 1 << 11 # always 1
|
info |= 1 << 11 # always 1
|
||||||
info |= 0 << 10 # 0=Closed GOP, 1=Open GOP
|
info |= 0 << 10 # 0=Closed GOP, 1=Open GOP
|
||||||
|
@ -39,15 +36,16 @@ def make_info(frames):
|
||||||
info |= new_gop << 8
|
info |= new_gop << 8
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
def make_flags(frames):
|
def make_flags(frames):
|
||||||
flags = []
|
flags = []
|
||||||
for frame in frames:
|
for frame in frames:
|
||||||
needs_prev = False
|
needs_prev = False
|
||||||
progressive=not int(frame['interlaced_frame'])
|
progressive = not int(frame["interlaced_frame"])
|
||||||
pt=pict_types[frame['pict_type']]
|
pt = pict_types[frame["pict_type"]]
|
||||||
reserved = 0b00
|
reserved = 0b00
|
||||||
tff=int(frame['top_field_first'])
|
tff = int(frame["top_field_first"])
|
||||||
rff=int(frame['repeat_pict'])
|
rff = int(frame["repeat_pict"])
|
||||||
flag = 0b0
|
flag = 0b0
|
||||||
flag |= (not needs_prev) << 7
|
flag |= (not needs_prev) << 7
|
||||||
flag |= progressive << 6
|
flag |= progressive << 6
|
||||||
|
@ -61,9 +59,9 @@ def make_flags(frames):
|
||||||
|
|
||||||
def make_line(frames, stream):
|
def make_line(frames, stream):
|
||||||
info = f"{make_info(frames):03x}"
|
info = f"{make_info(frames):03x}"
|
||||||
matrix=colorspace[stream['color_space']]
|
matrix = colorspace[stream["color_space"]]
|
||||||
file = 0
|
file = 0
|
||||||
position=frames[0]['pkt_pos']
|
position = frames[0]["pkt_pos"]
|
||||||
skip = 0
|
skip = 0
|
||||||
vob = 0
|
vob = 0
|
||||||
cell = 0
|
cell = 0
|
||||||
|
@ -72,16 +70,27 @@ def make_line(frames,stream):
|
||||||
|
|
||||||
|
|
||||||
def get_frames(path):
|
def get_frames(path):
|
||||||
proc=SP.Popen([
|
proc = SP.Popen(
|
||||||
|
[
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-probesize", str(0x7fffffff),
|
"-probesize",
|
||||||
"-analyzeduration", str(0x7fffffff),
|
str(0x7FFFFFFF),
|
||||||
"-v","fatal",
|
"-analyzeduration",
|
||||||
"-i",path,
|
str(0x7FFFFFFF),
|
||||||
"-select_streams","v:0",
|
"-v",
|
||||||
|
"fatal",
|
||||||
|
"-i",
|
||||||
|
path,
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
"-show_frames",
|
"-show_frames",
|
||||||
"-print_format","compact"
|
"-print_format",
|
||||||
],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0)
|
"compact",
|
||||||
|
],
|
||||||
|
stdout=SP.PIPE,
|
||||||
|
stdin=SP.DEVNULL,
|
||||||
|
bufsize=0,
|
||||||
|
)
|
||||||
data = None
|
data = None
|
||||||
for line in proc.stdout:
|
for line in proc.stdout:
|
||||||
line = str(line, "utf8").strip().split("|")
|
line = str(line, "utf8").strip().split("|")
|
||||||
|
@ -92,30 +101,44 @@ def get_frames(path):
|
||||||
exit(ret)
|
exit(ret)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def get_streams(path):
|
def get_streams(path):
|
||||||
proc=SP.Popen([
|
proc = SP.Popen(
|
||||||
|
[
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-probesize", str(0x7fffffff),
|
"-probesize",
|
||||||
"-analyzeduration", str(0x7fffffff),
|
str(0x7FFFFFFF),
|
||||||
"-v","fatal",
|
"-analyzeduration",
|
||||||
"-i",path,
|
str(0x7FFFFFFF),
|
||||||
"-select_streams","v:0",
|
"-v",
|
||||||
|
"fatal",
|
||||||
|
"-i",
|
||||||
|
path,
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
"-show_streams",
|
"-show_streams",
|
||||||
"-show_format",
|
"-show_format",
|
||||||
"-print_format","json"
|
"-print_format",
|
||||||
],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0)
|
"json",
|
||||||
|
],
|
||||||
|
stdout=SP.PIPE,
|
||||||
|
stdin=SP.DEVNULL,
|
||||||
|
bufsize=0,
|
||||||
|
)
|
||||||
data = json.load(proc.stdout)
|
data = json.load(proc.stdout)
|
||||||
ret = proc.wait()
|
ret = proc.wait()
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
exit(ret)
|
exit(ret)
|
||||||
return data['streams'],data['format']
|
return data["streams"], data["format"]
|
||||||
|
|
||||||
|
|
||||||
def make_header(file):
|
def make_header(file):
|
||||||
return ["DGIndexProjectFile16", "1", os.path.abspath(file)]
|
return ["DGIndexProjectFile16", "1", os.path.abspath(file)]
|
||||||
|
|
||||||
|
|
||||||
def make_settings(stream):
|
def make_settings(stream):
|
||||||
pict_size = "x".join(map(str, [stream["width"], stream["height"]]))
|
pict_size = "x".join(map(str, [stream["width"], stream["height"]]))
|
||||||
frame_rate = list(map(int,stream['r_frame_rate'].split("/")))
|
frame_rate = list(map(int, stream["r_frame_rate"].split("/")))
|
||||||
frame_rate = (frame_rate[0] * 1000) // frame_rate[1]
|
frame_rate = (frame_rate[0] * 1000) // frame_rate[1]
|
||||||
frame_rate = f"{frame_rate} ({stream['r_frame_rate']})"
|
frame_rate = f"{frame_rate} ({stream['r_frame_rate']})"
|
||||||
header = [
|
header = [
|
||||||
|
@ -129,37 +152,46 @@ def make_settings(stream):
|
||||||
("Picture_Size", pict_size),
|
("Picture_Size", pict_size),
|
||||||
("Field_Operation", 0), # Honor Pulldown Flags
|
("Field_Operation", 0), # Honor Pulldown Flags
|
||||||
("Frame_Rate", frame_rate),
|
("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}"
|
yield f"{k}={v}"
|
||||||
|
|
||||||
|
|
||||||
def gen_d2v(path):
|
def gen_d2v(path):
|
||||||
yield from make_header(path)
|
yield from make_header(path)
|
||||||
yield ""
|
yield ""
|
||||||
streams, fmt = get_streams(path)
|
streams, fmt = get_streams(path)
|
||||||
stream=[s for s in streams if s['codec_type']=='video'][0]
|
stream = [s for s in streams if s["codec_type"] == "video"][0]
|
||||||
stream['index']=str(stream['index'])
|
stream["index"] = str(stream["index"])
|
||||||
yield from make_settings(stream)
|
yield from make_settings(stream)
|
||||||
yield ""
|
yield ""
|
||||||
line_buffer = []
|
line_buffer = []
|
||||||
frames = get_frames(path)
|
frames = get_frames(path)
|
||||||
prog_bar=tqdm(frames,total=int(fmt['size']),unit_divisor=1024,unit_scale=True,unit="iB",dec="Writing d2v")
|
prog_bar = tqdm(
|
||||||
|
frames,
|
||||||
|
total=int(fmt["size"]),
|
||||||
|
unit_divisor=1024,
|
||||||
|
unit_scale=True,
|
||||||
|
unit="iB",
|
||||||
|
desc="Writing d2v",
|
||||||
|
)
|
||||||
for line in prog_bar:
|
for line in prog_bar:
|
||||||
if 'frame' not in line:
|
if "frame" not in line:
|
||||||
continue
|
continue
|
||||||
frame=line['frame']
|
frame = line["frame"]
|
||||||
prog_bar.n=min(max(prog_bar.n,int(frame['pkt_pos'])),int(fmt['size']))
|
prog_bar.n = min(max(prog_bar.n, int(frame["pkt_pos"])), int(fmt["size"]))
|
||||||
prog_bar.update(0)
|
prog_bar.update(0)
|
||||||
if frame['stream_index']!=stream['index']:
|
if frame["stream_index"] != stream["index"]:
|
||||||
continue
|
continue
|
||||||
if frame['pict_type']=="I" and line_buffer:
|
if frame["pict_type"] == "I" and line_buffer:
|
||||||
yield make_line(line_buffer, stream)
|
yield make_line(line_buffer, stream)
|
||||||
line_buffer.clear()
|
line_buffer.clear()
|
||||||
line_buffer.append(frame)
|
line_buffer.append(frame)
|
||||||
prog_bar.close()
|
prog_bar.close()
|
||||||
yield None
|
yield None
|
||||||
|
|
||||||
|
|
||||||
def make_d2v(path):
|
def make_d2v(path):
|
||||||
outfile = os.path.splitext(os.path.basename(path))[0]
|
outfile = os.path.splitext(os.path.basename(path))[0]
|
||||||
outfile = os.path.extsep.join([outfile, "d2v"])
|
outfile = os.path.extsep.join([outfile, "d2v"])
|
||||||
|
|
74
vob_demux.py
74
vob_demux.py
|
@ -5,28 +5,40 @@ import subprocess as SP
|
||||||
|
|
||||||
|
|
||||||
def get_streams(path):
|
def get_streams(path):
|
||||||
proc=SP.Popen([
|
proc = SP.Popen(
|
||||||
|
[
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-probesize", str(0x7fffffff),
|
"-probesize",
|
||||||
"-analyzeduration", str(0x7fffffff),
|
str(0x7FFFFFFF),
|
||||||
"-v","fatal",
|
"-analyzeduration",
|
||||||
"-i",path,
|
str(0x7FFFFFFF),
|
||||||
|
"-v",
|
||||||
|
"fatal",
|
||||||
|
"-i",
|
||||||
|
path,
|
||||||
"-show_streams",
|
"-show_streams",
|
||||||
"-show_format",
|
"-show_format",
|
||||||
"-print_format","json"
|
"-print_format",
|
||||||
],stdout=SP.PIPE,stdin=SP.DEVNULL,bufsize=0)
|
"json",
|
||||||
|
],
|
||||||
|
stdout=SP.PIPE,
|
||||||
|
stdin=SP.DEVNULL,
|
||||||
|
bufsize=0,
|
||||||
|
)
|
||||||
data = json.load(proc.stdout)
|
data = json.load(proc.stdout)
|
||||||
ret = proc.wait()
|
ret = proc.wait()
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
return [], {}
|
return [], {}
|
||||||
return data['streams'],data['format']
|
return data["streams"], data["format"]
|
||||||
|
|
||||||
|
|
||||||
types = {
|
types = {
|
||||||
'mpeg2video': 'm2v',
|
"mpeg2video": "m2v",
|
||||||
'ac3': 'ac3',
|
"ac3": "ac3",
|
||||||
'dvd_subtitle': 'sup',
|
"dvd_subtitle": "sup",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def demux(path):
|
def demux(path):
|
||||||
folder = os.path.dirname(path)
|
folder = os.path.dirname(path)
|
||||||
basename = os.path.splitext(os.path.basename(path))[0]
|
basename = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
@ -34,30 +46,42 @@ def demux(path):
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-y",
|
"-y",
|
||||||
"-strict","-2",
|
# "-fflags","+genpts+igndts",
|
||||||
"-fflags","+genpts",
|
"-probesize",
|
||||||
"-probesize", str(0x7fffffff),
|
str(0x7FFFFFFF),
|
||||||
"-analyzeduration", str(0x7fffffff),
|
"-analyzeduration",
|
||||||
"-i",path,
|
str(0x7FFFFFFF),
|
||||||
"-scodec","copy",
|
"-i",
|
||||||
"-vcodec","copy",
|
path,
|
||||||
"-acodec","copy",
|
"-strict",
|
||||||
|
"-2",
|
||||||
|
"-vcodec",
|
||||||
|
"copy",
|
||||||
|
"-acodec",
|
||||||
|
"copy",
|
||||||
|
"-scodec",
|
||||||
|
"copy",
|
||||||
]
|
]
|
||||||
need_ffmpeg = False
|
need_ffmpeg = False
|
||||||
for stream in streams:
|
for stream in streams:
|
||||||
codec=stream['codec_name']
|
codec = stream["codec_name"]
|
||||||
ext = types.get(codec, codec)
|
ext = types.get(codec, codec)
|
||||||
idx=stream['index']
|
idx = stream["index"]
|
||||||
hex_id=stream['id']
|
hex_id = stream["id"]
|
||||||
codec_name=stream['codec_long_name']
|
codec_name = stream["codec_long_name"]
|
||||||
outfile = os.path.join(folder, f"{basename}_{idx}_{hex_id}")
|
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":
|
if codec == "dvd_subtitle":
|
||||||
SP.check_call([
|
SP.check_call([
|
||||||
"mencoder",path,"-vobsuboutindex",str(idx),"-vobsubout", outfile,"-nosound","-ovc", "copy", "-o",os.devnull
|
"mencoder",path,"-vobsuboutindex",str(idx),"-vobsubout", outfile,"-nosound","-ovc", "copy", "-o",os.devnull
|
||||||
])
|
])
|
||||||
continue
|
continue
|
||||||
print(idx,hex_id,codec_name,codec)
|
cmd += ["-map", f"0:#{hex_id}", "-strict", "-2", outfile + f".{ext}"]
|
||||||
cmd+=["-map",f"0:#{hex_id}",outfile+f".{ext}"]
|
|
||||||
need_ffmpeg = True
|
need_ffmpeg = True
|
||||||
if need_ffmpeg:
|
if need_ffmpeg:
|
||||||
SP.check_call(cmd)
|
SP.check_call(cmd)
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
demux(sys.argv[1])
|
Loading…
Reference in a new issue