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