1 /*
2   mkvmerge -- utility for splicing together matroska files
3   from component media subtypes
4 
5   Distributed under the GPL v2
6   see the file COPYING for details
7   or visit https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
8 
9   helper functions for chapters on DVDs
10 
11   Written by Moritz Bunkus <moritz@bunkus.org>.
12 */
13 
14 #include "common/common_pch.h"
15 
16 #if defined(HAVE_DVDREAD)
17 
18 #include <dvdread/dvd_reader.h>
19 #include <dvdread/ifo_read.h>
20 #include <dvdread/ifo_print.h>
21 
22 #include <QRegularExpression>
23 
24 #include "common/at_scope_exit.h"
25 #include "common/chapters/chapters.h"
26 #include "common/chapters/dvd.h"
27 #include "common/path.h"
28 #include "common/qt.h"
29 #include "common/strings/parsing.h"
30 #include "common/timestamp.h"
31 
32 namespace mtx::chapters {
33 
34 namespace {
35 
36 timestamp_c
frames_to_timestamp_ns(unsigned int num_frames,unsigned int fps)37 frames_to_timestamp_ns(unsigned int num_frames,
38                        unsigned int fps) {
39   auto factor = fps == 30 ? 1001 : 1000;
40   return timestamp_c::ns(1000000ull * factor * num_frames / (fps ? fps : 1));
41 }
42 
43 } // anonymous namespace
44 
45 
46 std::vector<std::vector<timestamp_c>>
parse_dvd(std::string const & file_name)47 parse_dvd(std::string const &file_name) {
48   dvd_reader_t *dvd{};
49   ifo_handle_t *vmg{};
50 
51   at_scope_exit_c global_cleanup{[dvd, vmg]() {
52     if (vmg)
53       ifoClose(vmg);
54     if (dvd)
55       DVDClose(dvd);
56   }};
57 
58   auto error = fmt::format(Y("Could not open '{0}' for reading.\n"), file_name);
59 
60   dvd = DVDOpen(file_name.c_str());
61   if (!dvd)
62     throw parser_x{error};
63 
64   vmg = ifoOpen(dvd, 0);
65   if (!vmg)
66     throw parser_x{error};
67 
68   std::vector<std::vector<timestamp_c>> titles_and_timestamps;
69   titles_and_timestamps.reserve(vmg->tt_srpt->nr_of_srpts);
70 
71   for (auto title = 0; title < vmg->tt_srpt->nr_of_srpts; ++title) {
72     auto vts = ifoOpen(dvd, vmg->tt_srpt->title[title].title_set_nr);
73 
74     at_scope_exit_c title_cleanup{[vts]() {
75       if (vts)
76         ifoClose(vts);
77     }};
78 
79     if (!vts)
80       throw parser_x{error};
81 
82     titles_and_timestamps.emplace_back();
83 
84     auto &timestamps       = titles_and_timestamps.back();
85     auto ttn               = vmg->tt_srpt->title[title].vts_ttn;
86     auto vts_ptt_srpt      = vts->vts_ptt_srpt;
87     auto overall_frames    = 0u;
88     auto fps               = 0u; // This should be consistent as DVDs are either NTSC or PAL
89 
90     for (auto chapter = 0; chapter < vmg->tt_srpt->title[title].nr_of_ptts - 1; chapter++) {
91       auto pgc_id        = vts_ptt_srpt->title[ttn - 1].ptt[chapter].pgcn;
92       auto pgn           = vts_ptt_srpt->title[ttn - 1].ptt[chapter].pgn;
93       auto cur_pgc       = vts->vts_pgcit->pgci_srp[pgc_id - 1].pgc;
94       auto start_cell    = cur_pgc->program_map[pgn - 1] - 1;
95       pgc_id             = vts_ptt_srpt->title[ttn - 1].ptt[chapter + 1].pgcn;
96       pgn                = vts_ptt_srpt->title[ttn - 1].ptt[chapter + 1].pgn;
97       cur_pgc            = vts->vts_pgcit->pgci_srp[pgc_id - 1].pgc;
98       auto end_cell      = cur_pgc->program_map[pgn - 1] - 2;
99       auto cur_frames    = 0u;
100 
101       for (auto cur_cell = start_cell; cur_cell <= end_cell; cur_cell++) {
102         auto dt        = &cur_pgc->cell_playback[cur_cell].playback_time;
103         auto hour      = ((dt->hour    & 0xf0) >> 4) * 10 + (dt->hour    & 0x0f);
104         auto minute    = ((dt->minute  & 0xf0) >> 4) * 10 + (dt->minute  & 0x0f);
105         auto second    = ((dt->second  & 0xf0) >> 4) * 10 + (dt->second  & 0x0f);
106         fps            = ((dt->frame_u & 0xc0) >> 6) == 1 ? 25 : 30; // by definition
107         cur_frames    += ((hour * 60 * 60) + minute * 60 + second) * fps;
108         cur_frames    += ((dt->frame_u & 0x30) >> 4) * 10 + (dt->frame_u & 0x0f);
109       }
110 
111       timestamps.emplace_back(frames_to_timestamp_ns(overall_frames, fps));
112 
113       overall_frames += cur_frames;
114     }
115 
116     timestamps.emplace_back(frames_to_timestamp_ns(overall_frames, fps));
117   }
118 
119   return titles_and_timestamps;
120 }
121 
122 std::shared_ptr<libmatroska::KaxChapters>
maybe_parse_dvd(std::string const & file_name,mtx::bcp47::language_c const & language)123 maybe_parse_dvd(std::string const &file_name,
124                 mtx::bcp47::language_c const &language) {
125   auto title             = 1u;
126   auto cleaned_file_name = file_name;
127   auto matches           = QRegularExpression{"(.+):([0-9]+)$"}.match(Q(cleaned_file_name));
128 
129   if (matches.hasMatch()) {
130     cleaned_file_name = to_utf8(matches.captured(1));
131 
132     if (!mtx::string::parse_number(to_utf8(matches.captured(2)), title) || (title < 1))
133       throw parser_x{fmt::format(Y("'{0}' is not a valid DVD title number."), to_utf8(matches.captured(2)))};
134   }
135 
136   auto dvd_dir = mtx::fs::to_path(cleaned_file_name);
137 
138   if (Q(cleaned_file_name).contains(QRegularExpression{"\\.(bup|ifo|vob)$", QRegularExpression::CaseInsensitiveOption}))
139     dvd_dir = dvd_dir.parent_path();
140 
141   else if (   !std::filesystem::is_directory(dvd_dir)
142            || (   !std::filesystem::is_regular_file(dvd_dir / "VIDEO_TS.IFO")
143                && !std::filesystem::is_regular_file(dvd_dir / "VIDEO_TS" / "VIDEO_TS.IFO")))
144     return {};
145 
146   auto titles_and_timestamps = parse_dvd(dvd_dir.u8string());
147 
148   if (title > titles_and_timestamps.size())
149     throw parser_x{fmt::format(Y("The title number '{0}' is higher than the number of titles on the DVD ({1})."), title, titles_and_timestamps.size())};
150 
151   return create_editions_and_chapters({ titles_and_timestamps[title - 1] }, language, {});
152 }
153 
154 }
155 
156 #endif  // HAVE_DVDREAD
157