Link to API token URL
[tmdb2mkvtags.git] / tmdb2mkvtags.php
1 #!/usr/bin/env php
2 <?php
3 /**
4  * Generate a Matroska tags file from TMDb information
5  *
6  * PHP version 7
7  *
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/
13  */
14 if ($argc < 3) {
15     fwrite(STDERR, "Usage: tmdb2mkvtags.php LANGUAGE \"MOVIE TITLE\" [OUTDIR]\n");
16     exit(1);
17 }
18
19 $apiToken = null;
20 $language = $argv[1];
21 $title    = $argv[2];
22 $outdir  = null;
23 if ($argc == 4) {
24     $outdir = $argv[3];
25 }
26
27
28 $configFiles = [];
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';
34 }
35 $configFiles[] = '/etc/tmdb2mkvtags.config.php';
36 foreach ($configFiles as $configFile) {
37     if (file_exists($configFile)) {
38         include_once $configFile;
39         break;
40     }
41 }
42 if ($apiToken === null) {
43     fwrite(STDERR, "API token is not set\n");
44     fwrite(
45         STDERR,
46         "Configuration files tried:\n " . implode("\n ", $configFiles) . "\n"
47     );
48     exit(2);
49 }
50
51
52 $movies = queryTmdb(
53     '/3/search/movie'
54     . '?query=' . urlencode($title)
55     . '&language=' . urlencode($language)
56     . '&include_adult=1'
57 );
58
59 if ($movies->total_results == 0) {
60     fwrite(STDERR, "No movies found\n");
61     exit(20);
62
63 } else if ($movies->total_results == 1) {
64     $movie = $movies->results[0];
65
66 } else {
67     $page = 1;
68     $itemsPerPage = 0;
69     do {
70         fwrite(STDERR, sprintf("Found %d movies\n", $movies->total_results));
71         foreach ($movies->results as $key => $movie) {
72             fwrite(
73                 STDERR,
74                 sprintf(
75                     "[%2d] %s\n",
76                     $key + ($page -1) * $itemsPerPage,
77                     $movie->title
78                 )
79             );
80         }
81         if ($page > 1) {
82             fwrite(STDERR, "p: previous page\n");
83         }
84         if ($movies->total_pages > $page) {
85             $itemsPerPage = count($movies->results);
86             fwrite(STDERR, "n: next page\n");
87         }
88         fwrite(STDERR, "\n");
89         fwrite(STDERR, 'Your selection: ');
90         $cmd = readline();
91         if (is_numeric($cmd)) {
92             $num = $cmd - ($page - 1) * $itemsPerPage;
93             if (isset($movies->results[$num])) {
94                 $movie = $movies->results[$num];
95                 break;
96             }
97             fwrite(STDERR, "Invalid selection $num\n");
98         } else if ($cmd == 'n' && $movies->total_pages > $page) {
99             $page++;
100         } else if ($cmd == 'p' && $page > 1) {
101             $page--;
102         } else if ($cmd == 'q' || $cmd == 'quit' || $cmd == 'exit') {
103             exit(30);
104         }
105
106         $movies = queryTmdb(
107             '/3/search/movie'
108             . '?query=' . urlencode($title)
109             . '&language=' . urlencode($language)
110             . '&include_adult=1'
111             . '&page=' . $page
112         );
113     } while (true);
114 }
115
116
117 $details = queryTmdb('3/movie/' . $movie->id . '?language=' . $language);
118 $credits = queryTmdb('3/movie/' . $movie->id . '/credits?language=' . $language);
119
120 $downloadImages = true;
121
122 $xml = new MkvTagXMLWriter();
123 if ($outdir === '-') {
124     $xml->openMemory();
125     $downloadImages = false;
126     fwrite(STDERR, "Not downloading images\n");
127 } else {
128     if ($outdir === null) {
129         $outdir = trim(str_replace('/', ' ', $movie->title));
130     }
131     $outdir = rtrim($outdir, '/') . '/';
132     if (is_file($outdir)) {
133         fwrite(STDERR, "Error: Output directory is a file\n");
134         exit(2);
135     }
136     if (!is_dir($outdir)) {
137         mkdir($outdir);
138     }
139     $outfile = $outdir . 'mkvtags.xml';
140     $xml->openURI($outfile);
141 }
142
143 $xml->setIndent(true);
144 $xml->startDocument("1.0");
145 $xml->writeRaw("<!DOCTYPE Tags SYSTEM \"matroskatags.dtd\">\n");
146
147 $xml->startElement("Tags");
148
149 if ($details->belongs_to_collection) {
150     $xml->startComment();
151     $xml->text('Collection information');
152     $xml->endComment();
153
154     $xml->startElement("Tag");
155     $xml->targetType(70);
156     $xml->simple('TITLE', $details->belongs_to_collection->name, $language);
157     $xml->endElement();
158 }
159
160
161 $xml->startComment();
162 $xml->text('Movie information');
163 $xml->endComment();
164
165 $xml->startElement("Tag");
166
167 $xml->targetType(50);
168 $xml->simple('TITLE', $movie->title, $language);
169 if ($details->tagline) {
170     $xml->simple('SUBTITLE', $details->tagline, $language);
171 }
172 $xml->simple('SYNOPSIS', $movie->overview, $language);
173
174 $xml->simple('DATE_RELEASED', $movie->release_date);
175
176 foreach ($details->genres as $genre) {
177     $xml->simple('GENRE', $genre->name, $language);
178 }
179
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);
183
184 if ($language != $movie->original_language) {
185     $xml->startElement('Simple');
186     $xml->startElement('Name');
187     $xml->text('ORIGINAL');
188     $xml->endElement();
189     $xml->simple('TITLE', $movie->original_title, $movie->original_language);
190     $xml->endElement();//Simple
191 }
192
193
194 foreach ($credits->cast as $actor) {
195     $xml->actor($actor->name, $actor->character, $language);
196 }
197
198 //map tmdb job to matroska tags
199 $crewMap = [
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',
211 ];
212 foreach ($credits->crew as $crewmate) {
213     if (isset($crewMap[$crewmate->job])) {
214         $xml->simple($crewMap[$crewmate->job], $crewmate->name);
215     }
216 }
217
218
219 $xml->endElement();//Tag
220 $xml->endElement();//Tags
221 $xml->endDocument();
222
223 if ($outdir === null) {
224     echo $xml->outputMemory();
225 } else {
226     $xml->flush();
227 }
228
229
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]);
238                 }
239             }
240         }
241     }
242
243     if ($details->poster_path) {
244         $size = $tmdbConfig->images->poster_sizes[
245             array_key_last($tmdbConfig->images->poster_sizes)
246         ];
247         $url = $tmdbConfig->images->secure_base_url . $size . $details->poster_path;
248         $imagePath = $outdir
249             . 'cover.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
250         if (!file_exists($imagePath)) {
251             file_put_contents($imagePath, file_get_contents($url));
252         }
253     }
254
255     if ($details->backdrop_path) {
256         $size = $tmdbConfig->images->backdrop_sizes[
257             array_key_last($tmdbConfig->images->backdrop_sizes)
258         ];
259         $url = $tmdbConfig->images->secure_base_url
260             . $size . $details->backdrop_path;
261         $imagePath = $outdir
262             . 'backdrop.' . pathinfo($details->poster_path, PATHINFO_EXTENSION);
263         if (!file_exists($imagePath)) {
264             file_put_contents($imagePath, file_get_contents($url));
265         }
266     }
267 }
268
269
270 if ($outdir !== '-') {
271     $fulldir = realpath($outdir);
272     fwrite(STDERR, "Files written into directory:\n$fulldir\n");
273 }
274
275
276
277 //var_dump($credits);
278 //var_dump($movie, $details);
279
280
281 function queryTmdb($path)
282 {
283     global $apiToken;
284
285     $url = 'https://api.themoviedb.org/' . $path;
286     $ctx = stream_context_create(
287         [
288             'http' => [
289                 'timeout'       => 5,
290                 'ignore_errors' => true,
291                 'header'        => 'Authorization: Bearer ' . $apiToken
292             ]
293         ]
294     );
295     $res = file_get_contents($url, false, $ctx);
296     list(, $statusCode) = explode(' ', $http_response_header[0]);
297     $data = json_decode($res);
298
299     if ($statusCode != 200) {
300         if (isset($data->status_code) && isset($data->status_message)) {
301             throw new Exception(
302                 'API error: ' . $data->status_code . ' ' . $data->status_message
303             );
304         }
305         throw new Exception('Error querying API: ' . $statusCode, $statusCode);
306     }
307     return $data;
308 }
309
310 class MkvTagXMLWriter extends XMLWriter
311 {
312     public function actor($actorName, $characterName)
313     {
314         $this->startElement('Simple');
315         $this->startElement('Name');
316         $this->text('ACTOR');
317         $this->endElement();
318         $this->startElement('String');
319         $this->text($actorName);
320         $this->endElement();
321         $this->simple('CHARACTER', $characterName);
322         $this->endElement();//Simple
323     }
324
325     public function simple($key, $value, $language = null)
326     {
327         $this->startElement('Simple');
328         $this->startElement('Name');
329         $this->text($key);
330         $this->endElement();
331         $this->startElement('String');
332         $this->text($value);
333         $this->endElement();
334         if ($language) {
335             $this->startElement('TagLanguage');
336             $this->text($language);
337             $this->endElement();
338         }
339         $this->endElement();
340     }
341
342     public function targetType($value)
343     {
344         $this->startElement('Targets');
345         $this->startElement('TargetType');
346         $this->text($value);
347         $this->endElement();
348         $this->endElement();
349     }
350 }
351 ?>