4 * Generate a Matroska tags file from TMDb information
8 * @author Christian Weiske <cweiske@cweiske.de>
9 * @license https://www.gnu.org/licenses/gpl-3.0.txt GPL-3.0-or-later
10 * @link https://www.themoviedb.org/
11 * @link https://www.matroska.org/technical/tagging.html
12 * @link https://developers.themoviedb.org/3/
15 fwrite(STDERR, "Usage: tmdb2mkvtags.php LANGUAGE \"MOVIE TITLE\" [OUTDIR]\n");
29 $configFiles[] = preg_replace('#.php$#', '', $argv[0]) . '.config.php';
30 if (isset($_SERVER['XDG_CONFIG_HOME'])) {
31 $configFiles[] = $_SERVER['XDG_CONFIG_HOME'] . '/tmdb2mkvtags.config.php';
32 } else if (isset($_SERVER['HOME'])) {
33 $configFiles[] = $_SERVER['HOME'] . '/.config/tmdb2mkvtags.config.php';
35 $configFiles[] = '/etc/tmdb2mkvtags.config.php';
36 foreach ($configFiles as $configFile) {
37 if (file_exists($configFile)) {
38 include_once $configFile;
42 if ($apiToken === null) {
43 fwrite(STDERR, "API token is not set\n");
46 "Configuration files tried:\n " . implode("\n ", $configFiles) . "\n"
54 . '?query=' . urlencode($title)
55 . '&language=' . urlencode($language)
59 if ($movies->total_results == 0) {
60 fwrite(STDERR, "No movies found\n");
63 } else if ($movies->total_results == 1) {
64 $movie = $movies->results[0];
70 fwrite(STDERR, sprintf("Found %d movies\n", $movies->total_results));
71 foreach ($movies->results as $key => $movie) {
76 $key + ($page -1) * $itemsPerPage,
82 fwrite(STDERR, "p: previous page\n");
84 if ($movies->total_pages > $page) {
85 $itemsPerPage = count($movies->results);
86 fwrite(STDERR, "n: next page\n");
89 fwrite(STDERR, 'Your selection: ');
91 if (is_numeric($cmd)) {
92 $num = $cmd - ($page - 1) * $itemsPerPage;
93 if (isset($movies->results[$num])) {
94 $movie = $movies->results[$num];
97 fwrite(STDERR, "Invalid selection $num\n");
98 } else if ($cmd == 'n' && $movies->total_pages > $page) {
100 } else if ($cmd == 'p' && $page > 1) {
102 } else if ($cmd == 'q' || $cmd == 'quit' || $cmd == 'exit') {
108 . '?query=' . urlencode($title)
109 . '&language=' . urlencode($language)
117 $details = queryTmdb('3/movie/' . $movie->id . '?language=' . $language);
118 $credits = queryTmdb('3/movie/' . $movie->id . '/credits?language=' . $language);
120 $downloadImages = true;
122 $xml = new MkvTagXMLWriter();
123 if ($outdir === '-') {
125 $downloadImages = false;
126 fwrite(STDERR, "Not downloading images\n");
128 if ($outdir === null) {
129 $outdir = trim(str_replace('/', ' ', $movie->title));
131 $outdir = rtrim($outdir, '/') . '/';
132 if (is_file($outdir)) {
133 fwrite(STDERR, "Error: Output directory is a file\n");
136 if (!is_dir($outdir)) {
139 $outfile = $outdir . 'mkvtags.xml';
140 $xml->openURI($outfile);
143 $xml->setIndent(true);
144 $xml->startDocument("1.0");
145 $xml->writeRaw("<!DOCTYPE Tags SYSTEM \"matroskatags.dtd\">\n");
147 $xml->startElement("Tags");
149 if ($details->belongs_to_collection) {
150 $xml->startComment();
151 $xml->text('Collection information');
154 $xml->startElement("Tag");
155 $xml->targetType(70);
156 $xml->simple('TITLE', $details->belongs_to_collection->name, $language);
161 $xml->startComment();
162 $xml->text('Movie information');
165 $xml->startElement("Tag");
167 $xml->targetType(50);
168 $xml->simple('TITLE', $movie->title, $language);
169 if ($details->tagline) {
170 $xml->simple('SUBTITLE', $details->tagline, $language);
172 $xml->simple('SYNOPSIS', $movie->overview, $language);
174 $xml->simple('DATE_RELEASED', $movie->release_date);
176 foreach ($details->genres as $genre) {
177 $xml->simple('GENRE', $genre->name, $language);
180 $xml->simple('RATING', $movie->vote_average / 2);//0-10 on TMDB, 0-5 mkv
181 $xml->simple('TMDB', 'movie/' . $movie->id);
182 $xml->simple('IMDB', $details->imdb_id);
184 if ($language != $movie->original_language) {
185 $xml->startElement('Simple');
186 $xml->startElement('Name');
187 $xml->text('ORIGINAL');
189 $xml->simple('TITLE', $movie->original_title, $movie->original_language);
190 $xml->endElement();//Simple
194 foreach ($credits->cast as $actor) {
195 $xml->actor($actor->name, $actor->character, $language);
198 //map tmdb job to matroska tags
200 'Art Direction' => 'ART_DIRECTOR',
201 'Costume Design' => 'COSTUME_DESIGNER',
202 'Director of Photography' => 'DIRECTOR_OF_PHOTOGRAPHY',
203 'Director' => 'DIRECTOR',
204 'Editor' => 'EDITED_BY',
205 'Novel' => 'WRITTEN_BY',
206 'Original Music Composer' => 'COMPOSER',
207 'Producer' => 'PRODUCER',
208 'Screenplay' => 'WRITTEN_BY',
209 'Sound' => 'COMPOSER',
210 'Theme Song Performance' => 'LEAD_PERFORMER',
212 foreach ($credits->crew as $crewmate) {
213 if (isset($crewMap[$crewmate->job])) {
214 $xml->simple($crewMap[$crewmate->job], $crewmate->name);
219 $xml->endElement();//Tag
220 $xml->endElement();//Tags
223 if ($outdir === null) {
224 echo $xml->outputMemory();
230 if ($downloadImages) {
231 //we take the largest scaled image, not the original image
232 $tmdbConfig = queryTmdb('3/configuration');
233 foreach ($tmdbConfig->images as $key => $sizes) {
234 if (is_array($sizes)) {
235 foreach ($sizes as $sizeKey => $value) {
236 if ($value == 'original') {
237 unset($tmdbConfig->images->$key[$sizeKey]);
243 if ($details->poster_path) {
244 $size = $tmdbConfig->images->poster_sizes[
245 array_key_last($tmdbConfig->images->poster_sizes)
247 $url = $tmdbConfig->images->secure_base_url . $size . $details->poster_path;
249 . 'cover.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
250 if (!file_exists($imagePath)) {
251 file_put_contents($imagePath, file_get_contents($url));
255 if ($details->backdrop_path) {
256 $size = $tmdbConfig->images->backdrop_sizes[
257 array_key_last($tmdbConfig->images->backdrop_sizes)
259 $url = $tmdbConfig->images->secure_base_url
260 . $size . $details->backdrop_path;
262 . 'backdrop.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
263 if (!file_exists($imagePath)) {
264 file_put_contents($imagePath, file_get_contents($url));
270 if ($outdir !== '-') {
271 $fulldir = realpath($outdir);
272 fwrite(STDERR, "Files written into directory:\n$fulldir\n");
277 //var_dump($credits);
278 //var_dump($movie, $details);
281 function queryTmdb($path)
285 $url = 'https://api.themoviedb.org/' . $path;
286 $ctx = stream_context_create(
290 'ignore_errors' => true,
291 'header' => 'Authorization: Bearer ' . $apiToken
295 $res = file_get_contents($url, false, $ctx);
296 list(, $statusCode) = explode(' ', $http_response_header[0]);
297 $data = json_decode($res);
299 if ($statusCode != 200) {
300 if (isset($data->status_code) && isset($data->status_message)) {
302 'API error: ' . $data->status_code . ' ' . $data->status_message
305 throw new Exception('Error querying API: ' . $statusCode, $statusCode);
310 class MkvTagXMLWriter extends XMLWriter
312 public function actor($actorName, $characterName)
314 $this->startElement('Simple');
315 $this->startElement('Name');
316 $this->text('ACTOR');
318 $this->startElement('String');
319 $this->text($actorName);
321 $this->simple('CHARACTER', $characterName);
322 $this->endElement();//Simple
325 public function simple($key, $value, $language = null)
327 $this->startElement('Simple');
328 $this->startElement('Name');
331 $this->startElement('String');
335 $this->startElement('TagLanguage');
336 $this->text($language);
342 public function targetType($value)
344 $this->startElement('Targets');
345 $this->startElement('TargetType');