vtskit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vtskit/__init__.py +25 -0
- vtskit/errors.py +8 -0
- vtskit/ifo.py +113 -0
- vtskit/lang.py +648 -0
- vtskit/menu.py +171 -0
- vtskit/nav.py +256 -0
- vtskit/ps.py +484 -0
- vtskit/spu.py +262 -0
- vtskit/subtitles.py +126 -0
- vtskit/titleset.py +967 -0
- vtskit/vm.py +482 -0
- vtskit/vmg.py +159 -0
- vtskit-0.1.0.dist-info/METADATA +205 -0
- vtskit-0.1.0.dist-info/RECORD +18 -0
- vtskit-0.1.0.dist-info/WHEEL +5 -0
- vtskit-0.1.0.dist-info/licenses/COPYING +674 -0
- vtskit-0.1.0.dist-info/licenses/COPYING.LESSER +165 -0
- vtskit-0.1.0.dist-info/top_level.txt +1 -0
vtskit/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""vtskit: DVD-Video titlesets in pure Python.
|
|
2
|
+
|
|
3
|
+
Steps 1 to 3 of the plan in the README, a disc with titles, menus,
|
|
4
|
+
subtitles and angles, and dvdauthor's command language:
|
|
5
|
+
|
|
6
|
+
* `vtskit.titleset`: a titleset written from its titles' program streams
|
|
7
|
+
(several parts in a row, with angle blocks) and its menus: the VOBs with
|
|
8
|
+
their navigation packs filled, the IFO and its BUP (`vtskit.menu`
|
|
9
|
+
describes a menu and its buttons, `vtskit.spu` draws the button overlay).
|
|
10
|
+
* `vtskit.vmg`: the video manager (VIDEO_TS.IFO, its BUP, the title
|
|
11
|
+
menu's VOB).
|
|
12
|
+
* `vtskit.subtitles`: subtitles added to a title's program stream, as
|
|
13
|
+
spumux does.
|
|
14
|
+
* `vtskit.vm`: the navigation commands a program chain carries, encoded;
|
|
15
|
+
`vtskit.lang` compiles dvdauthor's command language to them.
|
|
16
|
+
* `vtskit.nav`: the navigation packs of a VOB filled in (`vtskit.ps` reads
|
|
17
|
+
the program stream for both).
|
|
18
|
+
* `vtskit.ifo`: the chapters of a disc and where they start, read from its
|
|
19
|
+
IFO files.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .errors import VtsError
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = ["VtsError"]
|
vtskit/errors.py
ADDED
vtskit/ifo.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Read the chapters of a DVD-Video disc from its IFO files.
|
|
2
|
+
|
|
3
|
+
Where each chapter of each title starts, as a sector of the disc: what a
|
|
4
|
+
titleset writer is checked against, and what a layer break is chosen from.
|
|
5
|
+
Read-only; nothing here writes an IFO.
|
|
6
|
+
|
|
7
|
+
Sector numbers in the IFO files are relative: the VMG's title table gives
|
|
8
|
+
where each titleset starts (counted from VIDEO_TS.IFO), a titleset IFO
|
|
9
|
+
gives where its title VOBs start (VTS_NN_0.IFO at 0xC4, counted from the
|
|
10
|
+
titleset), and a cell's first sector, in the program chain's cell playback
|
|
11
|
+
table, is counted from the title VOBs.
|
|
12
|
+
|
|
13
|
+
(From oncemore's layerbreak.py, by the same author, relicensed under
|
|
14
|
+
LGPL-3.0-or-later for vtskit.)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import struct
|
|
19
|
+
|
|
20
|
+
from .errors import VtsError
|
|
21
|
+
|
|
22
|
+
SECTOR = 2048
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _u16(b, o):
|
|
26
|
+
return struct.unpack_from(">H", b, o)[0]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _u32(b, o):
|
|
30
|
+
return struct.unpack_from(">I", b, o)[0]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check(b, o, size, what):
|
|
34
|
+
if o < 0 or o + size > len(b):
|
|
35
|
+
raise VtsError(f"{what} is outside the IFO ({o} of {len(b)} bytes)")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def chapter_cells(ifo):
|
|
39
|
+
"""The chapters of a VTS_NN_0.IFO (its bytes): [(title number in the
|
|
40
|
+
titleset, chapter, first sector)], the sector relative to the start of
|
|
41
|
+
the title VOBs."""
|
|
42
|
+
if ifo[:12] != b"DVDVIDEO-VTS":
|
|
43
|
+
raise VtsError("not a titleset IFO")
|
|
44
|
+
try:
|
|
45
|
+
ptt = _u32(ifo, 0xC8) * SECTOR
|
|
46
|
+
pgcit = _u32(ifo, 0xCC) * SECTOR
|
|
47
|
+
_check(ifo, ptt, 8, "the chapter table")
|
|
48
|
+
_check(ifo, pgcit, 8, "the program chain table")
|
|
49
|
+
ntitles, end = _u16(ifo, ptt), ptt + _u32(ifo, ptt + 4) + 1
|
|
50
|
+
offsets = [ptt + _u32(ifo, ptt + 8 + 4 * k) for k in range(ntitles)] + [end]
|
|
51
|
+
out = []
|
|
52
|
+
for ttn in range(1, ntitles + 1):
|
|
53
|
+
a, b = offsets[ttn - 1], offsets[ttn]
|
|
54
|
+
for ch, o in enumerate(range(a, b - 3, 4), 1):
|
|
55
|
+
pgcn, pgn = _u16(ifo, o), _u16(ifo, o + 2)
|
|
56
|
+
if pgcn < 1 or pgn < 1:
|
|
57
|
+
raise VtsError(f"title {ttn} chapter {ch} points at no program")
|
|
58
|
+
pgc = pgcit + _u32(ifo, pgcit + 8 + 8 * (pgcn - 1) + 4)
|
|
59
|
+
_check(ifo, pgc, 0xEC, f"program chain {pgcn}")
|
|
60
|
+
entry = pgc + _u16(ifo, pgc + 0xE6) + pgn - 1
|
|
61
|
+
_check(ifo, entry, 1, f"program {pgn} of program chain {pgcn}")
|
|
62
|
+
cell = ifo[entry] # the program's entry cell
|
|
63
|
+
cpb = pgc + _u16(ifo, pgc + 0xE8) + 24 * (cell - 1)
|
|
64
|
+
_check(ifo, cpb, 24, f"cell {cell} of program chain {pgcn}")
|
|
65
|
+
out.append((ttn, ch, _u32(ifo, cpb + 8)))
|
|
66
|
+
return out
|
|
67
|
+
except struct.error as e:
|
|
68
|
+
raise VtsError(f"damaged titleset IFO: {e}") from None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def vts_bases(ifo_bytes):
|
|
72
|
+
"""{titleset: sector of its title VOBs, relative to VIDEO_TS.IFO} from
|
|
73
|
+
the VMG and VTS IFOs ({"VIDEO_TS.IFO": bytes, "VTS_01_0.IFO": bytes, ...})."""
|
|
74
|
+
vmg = ifo_bytes.get("VIDEO_TS.IFO", b"")
|
|
75
|
+
if vmg[:12] != b"DVDVIDEO-VMG":
|
|
76
|
+
raise VtsError("VIDEO_TS.IFO is not a DVD-Video IFO")
|
|
77
|
+
try:
|
|
78
|
+
tt = _u32(vmg, 0xC4) * SECTOR
|
|
79
|
+
_check(vmg, tt, 8, "the title table")
|
|
80
|
+
starts = {}
|
|
81
|
+
for k in range(_u16(vmg, tt)):
|
|
82
|
+
o = tt + 8 + 12 * k
|
|
83
|
+
_check(vmg, o, 12, f"title {k + 1} of the title table")
|
|
84
|
+
starts[vmg[o + 6]] = _u32(vmg, o + 8)
|
|
85
|
+
except struct.error as e:
|
|
86
|
+
raise VtsError(f"damaged VIDEO_TS.IFO: {e}") from None
|
|
87
|
+
out = {}
|
|
88
|
+
for vts, start in starts.items():
|
|
89
|
+
name = f"VTS_{vts:02d}_0.IFO"
|
|
90
|
+
if name not in ifo_bytes:
|
|
91
|
+
raise VtsError(f"the title table names titleset {vts}, and {name} is missing")
|
|
92
|
+
out[vts] = start + _u32(ifo_bytes[name], 0xC4)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def chapters(ifo_bytes):
|
|
97
|
+
"""Every chapter on the disc: [(titleset, title number, chapter, sector
|
|
98
|
+
relative to VIDEO_TS.IFO)], in disc order."""
|
|
99
|
+
out = []
|
|
100
|
+
for vts, base in sorted(vts_bases(ifo_bytes).items()):
|
|
101
|
+
for ttn, ch, s in chapter_cells(ifo_bytes[f"VTS_{vts:02d}_0.IFO"]):
|
|
102
|
+
out.append((vts, ttn, ch, base + s))
|
|
103
|
+
return sorted(out, key=lambda c: c[3])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def read_folder(video_ts):
|
|
107
|
+
"""The IFO files of a VIDEO_TS folder: {name: bytes}."""
|
|
108
|
+
out = {}
|
|
109
|
+
for n in os.listdir(video_ts):
|
|
110
|
+
if n == "VIDEO_TS.IFO" or (n.startswith("VTS_") and n.endswith("_0.IFO")):
|
|
111
|
+
with open(os.path.join(video_ts, n), "rb") as f:
|
|
112
|
+
out[n] = f.read()
|
|
113
|
+
return out
|