4 * Generate a Matroska tags file from TMDb information
6 * @link https://www.themoviedb.org/
7 * @link https://www.matroska.org/technical/tagging.html
8 * @link https://developers.themoviedb.org/3/
10 * @author Christian Weiske <cweiske@cweiske.de>
13 fwrite(STDERR, "Usage: tmdb2mkvtags.php LANGUAGE \"MOVIE TITLE\" [OUTDIR]\n");
27 $configFiles[] = preg_replace('#.php$#', '', $argv[0]) . '.config.php';
28 if (isset($_SERVER['XDG_CONFIG_HOME'])) {
29 $configFiles[] = $_SERVER['XDG_CONFIG_HOME'] . '/tmdb2mkvtags.config.php';
30 } else if (isset($_SERVER['HOME'])) {
31 $configFiles[] = $_SERVER['HOME'] . '/.config/tmdb2mkvtags.config.php';
33 $configFiles[] = '/etc/tmdb2mkvtags.config.php';
34 foreach ($configFiles as $configFile) {
35 if (file_exists($configFile)) {
36 require_once $configFile;
40 if ($apiToken === null) {
41 fwrite(STDERR, "API token is not set\n");
42 fwrite(STDERR, "Configuration files tried:\n " . implode("\n ", $configFiles) . "\n");
49 . '?query=' . urlencode($title)
50 . '&language=' . urlencode($language)
54 if ($movies->total_results == 0) {
55 fwrite(STDERR, "No movies found\n");
58 } else if ($movies->total_results == 1) {
59 $movie = $movies->results[0];
65 fwrite(STDERR, sprintf("Found %d movies\n", $movies->total_results));
66 foreach ($movies->results as $key => $movie) {
67 fwrite(STDERR, sprintf("[%2d] %s\n", $key + ($page -1) * $itemsPerPage, $movie->title));
70 fwrite(STDERR, "p: previous page\n");
72 if ($movies->total_pages > $page) {
73 $itemsPerPage = count($movies->results);
74 fwrite(STDERR, "n: next page\n");
77 fwrite(STDERR, 'Your selection: ');
79 if (is_numeric($cmd)) {
80 $num = $cmd - ($page - 1) * $itemsPerPage;
81 if (isset($movies->results[$num])) {
82 $movie = $movies->results[$num];
85 fwrite(STDERR, "Invalid selection $num\n");
86 } else if ($cmd == 'n' && $movies->total_pages > $page) {
88 } else if ($cmd == 'p' && $page > 1) {
90 } else if ($cmd == 'q' || $cmd == 'quit' || $cmd == 'exit') {
96 . '?query=' . urlencode($title)
97 . '&language=' . urlencode($language)
105 $details = queryTmdb('3/movie/' . $movie->id . '?language=' . $language);
106 $credits = queryTmdb('3/movie/' . $movie->id . '/credits?language=' . $language);
108 $downloadImages = true;
110 $xml = new MkvTagXMLWriter();
111 if ($outdir === '-') {
113 $downloadImages = false;
114 fwrite(STDERR, "Not downloading images\n");
116 if ($outdir === null) {
117 $outdir = trim(str_replace('/', ' ', $movie->title));
119 $outdir = rtrim($outdir, '/') . '/';
120 if (is_file($outdir)) {
121 fwrite(STDERR, "Error: Output directory is a file\n");
124 if (!is_dir($outdir)) {
127 $outfile = $outdir . 'mkvtags.xml';
128 $xml->openURI($outfile);
131 $xml->setIndent(true);
132 $xml->startDocument("1.0");
133 $xml->writeRaw("<!DOCTYPE Tags SYSTEM \"matroskatags.dtd\">\n");
135 $xml->startElement("Tags");
137 if ($details->belongs_to_collection) {
138 $xml->startComment();
139 $xml->text('Collection information');
142 $xml->startElement("Tag");
143 $xml->targetType(70);
144 $xml->simple('TITLE', $details->belongs_to_collection->name, $language);
149 $xml->startComment();
150 $xml->text('Movie information');
153 $xml->startElement("Tag");
155 $xml->targetType(50);
156 $xml->simple('TITLE', $movie->title, $language);
157 if ($details->tagline) {
158 $xml->simple('SUBTITLE', $details->tagline, $language);
160 $xml->simple('SYNOPSIS', $movie->overview, $language);
162 $xml->simple('DATE_RELEASED', $movie->release_date);
164 foreach ($details->genres as $genre) {
165 $xml->simple('GENRE', $genre->name, $language);
168 $xml->simple('RATING', $movie->vote_average / 2);//0-10 on TMDB, 0-5 mkv
169 $xml->simple('TMDB', 'movie/' . $movie->id);
170 $xml->simple('IMDB', $details->imdb_id);
172 if ($language != $movie->original_language) {
173 $xml->startElement('Simple');
174 $xml->startElement('Name');
175 $xml->text('ORIGINAL');
177 $xml->simple('TITLE', $movie->original_title, $movie->original_language);
178 $xml->endElement();//Simple
182 foreach ($credits->cast as $actor) {
183 $xml->actor($actor->name, $actor->character, $language);
186 //map tmdb job to matroska tags
188 'Art Direction' => 'ART_DIRECTOR',
189 'Costume Design' => 'COSTUME_DESIGNER',
190 'Director of Photography' => 'DIRECTOR_OF_PHOTOGRAPHY',
191 'Director' => 'DIRECTOR',
192 'Editor' => 'EDITED_BY',
193 'Novel' => 'WRITTEN_BY',
194 'Original Music Composer' => 'COMPOSER',
195 'Producer' => 'PRODUCER',
196 'Screenplay' => 'WRITTEN_BY',
197 'Sound' => 'COMPOSER',
198 'Theme Song Performance' => 'LEAD_PERFORMER',
200 foreach ($credits->crew as $crewmate) {
201 if (isset($crewMap[$crewmate->job])) {
202 $xml->simple($crewMap[$crewmate->job], $crewmate->name);
207 $xml->endElement();//Tag
208 $xml->endElement();//Tags
211 if ($outdir === null) {
212 echo $xml->outputMemory();
218 if ($downloadImages) {
219 //we take the largest scaled image, not the original image
220 $tmdbConfig = queryTmdb('3/configuration');
221 foreach ($tmdbConfig->images as $key => $sizes) {
222 if (is_array($sizes)) {
223 foreach ($sizes as $sizeKey => $value) {
224 if ($value == 'original') {
225 unset($tmdbConfig->images->$key[$sizeKey]);
231 if ($details->poster_path) {
232 $size = $tmdbConfig->images->poster_sizes[
233 array_key_last($tmdbConfig->images->poster_sizes)
235 $url = $tmdbConfig->images->secure_base_url . $size . $details->poster_path;
237 . 'cover.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
238 if (!file_exists($imagePath)) {
239 file_put_contents($imagePath, file_get_contents($url));
243 if ($details->backdrop_path) {
244 $size = $tmdbConfig->images->backdrop_sizes[
245 array_key_last($tmdbConfig->images->backdrop_sizes)
247 $url = $tmdbConfig->images->secure_base_url . $size . $details->backdrop_path;
249 . 'backdrop.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
250 if (!file_exists($imagePath)) {
251 file_put_contents($imagePath, file_get_contents($url));
257 if ($outdir !== '-') {
258 $fulldir = realpath($outdir);
259 fwrite(STDERR, "Files written into directory:\n$fulldir\n");
264 //var_dump($credits);
265 //var_dump($movie, $details);
268 function queryTmdb($path)
272 $url = 'https://api.themoviedb.org/' . $path;
273 $ctx = stream_context_create(
277 'ignore_errors' => true,
278 'header' => 'Authorization: Bearer ' . $apiToken
282 $res = file_get_contents($url, false, $ctx);
283 list(, $statusCode) = explode(' ', $http_response_header[0]);
284 $data = json_decode($res);
286 if ($statusCode != 200) {
287 if (isset($data->status_code) && isset($data->status_message)) {
289 'API error: ' . $data->status_code . ' ' . $data->status_message
292 throw new Exception('Error querying API: ' . $statusCode, $statusCode);
297 class MkvTagXMLWriter extends XMLWriter
299 public function actor($actorName, $characterName)
301 $this->startElement('Simple');
302 $this->startElement('Name');
303 $this->text('ACTOR');
305 $this->startElement('String');
306 $this->text($actorName);
308 $this->simple('CHARACTER', $characterName);
309 $this->endElement();//Simple
312 public function simple($key, $value, $language = null)
314 $this->startElement('Simple');
315 $this->startElement('Name');
318 $this->startElement('String');
322 $this->startElement('TagLanguage');
323 $this->text($language);
329 public function targetType($value)
331 $this->startElement('Targets');
332 $this->startElement('TargetType');