Initial working version
authorChristian Weiske <cweiske@cweiske.de>
Thu, 29 Apr 2021 20:45:54 +0000 (22:45 +0200)
committerChristian Weiske <cweiske@cweiske.de>
Thu, 29 Apr 2021 20:45:54 +0000 (22:45 +0200)
.gitignore [new file with mode: 0644]
README.rst [new file with mode: 0644]
matroskatags.dtd [new file with mode: 0644]
tmdb2mkvtags.config.php.dist [new file with mode: 0644]
tmdb2mkvtags.php [new file with mode: 0755]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d7a44e
--- /dev/null
@@ -0,0 +1,2 @@
+tmdb2mkvtags.config.php
+*.xml
diff --git a/README.rst b/README.rst
new file mode 100644 (file)
index 0000000..3748850
--- /dev/null
@@ -0,0 +1,16 @@
+============
+tmdb2mkvtags
+============
+
+Generate a Matroska tags file from TMDb__ information.
+
+__ https://www.themoviedb.org/
+
+
+Usage
+=====
+
+1. Copy ``tmdb2mkvtags.config.php.dist`` to ``tmdb2mkvtags.config.php`` and add your API token
+2. Run tmdb2mkvtags.php::
+
+     php tmdb2mkvtags.php de "James Bond Diamantenfieber" diamanten.xml
diff --git a/matroskatags.dtd b/matroskatags.dtd
new file mode 100644 (file)
index 0000000..5779fa6
--- /dev/null
@@ -0,0 +1,32 @@
+<!ELEMENT Tags (Tag*)>
+<!ELEMENT Tag (
+          Targets,
+          Simple*)>
+
+<!ELEMENT Targets (
+          TrackUID*,
+          ChapterUID*,
+          AttachmentUID*,
+          EditionUID*,
+          TargetType?,
+          TargetTypeValue?)>
+
+<!ELEMENT TrackUID (#PCDATA)>
+<!ELEMENT ChapterUID (#PCDATA)>
+<!ELEMENT AttachmentUID (#PCDATA)>
+<!ELEMENT EditionUID (#PCDATA)>
+<!ELEMENT TargetType (#PCDATA)>
+<!ELEMENT TargetTypeValue (#PCDATA)>
+
+<!ELEMENT Simple (
+          Name,
+          String?,
+          Binary?,
+          TagLanguage?,
+          DefaultLanguage?,
+          Simple*)>
+<!ELEMENT Name (#PCDATA)>
+<!ELEMENT String (#PCDATA)>
+<!ELEMENT Binary (#PCDATA)>
+<!ELEMENT TagLanguage (#PCDATA)>
+<!ELEMENT DefaultLanguage (#PCDATA)>
diff --git a/tmdb2mkvtags.config.php.dist b/tmdb2mkvtags.config.php.dist
new file mode 100644 (file)
index 0000000..c988c4e
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+$apiToken = 'FIXME';
+?>
diff --git a/tmdb2mkvtags.php b/tmdb2mkvtags.php
new file mode 100755 (executable)
index 0000000..9cd3822
--- /dev/null
@@ -0,0 +1,240 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Generate a Matroska tags file from TMDb information
+ *
+ * @link https://www.themoviedb.org/
+ * @link https://www.matroska.org/technical/tagging.html
+ * @link https://developers.themoviedb.org/3/
+ *
+ * @author Christian Weiske <cweiske@cweiske.de>
+ */
+if ($argc < 3) {
+    fwrite(STDERR, "Usage: tmdb2mkvtags.php LANGUAGE \"MOVIE TITLE\" [OUTFILE]\n");
+    exit(1);
+}
+
+$apiToken = null;
+$language = $argv[1];
+$title    = $argv[2];
+$outfile  = null;
+if ($argc == 4) {
+    $outfile = $argv[3];
+}
+
+$configFile = preg_replace('#.php$#', '', $argv[0]) . '.config.php';
+if (file_exists($configFile)) {
+    require_once $configFile;
+}
+if ($apiToken === null) {
+    fwrite(STDERR, "API token is not set\n");
+    exit(2);
+}
+
+
+$movies = queryTmdb(
+    '/3/search/movie'
+    . '?query=' . urlencode($title)
+    . '&language=' . urlencode($language)
+    . '&include_adult=1'
+);
+
+if ($movies->total_results == 0) {
+    fwrite(STDERR, "No movies found\n");
+    exit(20);
+
+} else if ($movies->total_results == 1) {
+    $movie = $movies->results[0];
+
+} else {
+    $page = 1;
+    $itemsPerPage = 0;
+    do {
+        fwrite(STDERR, sprintf("Found %d movies\n", $movies->total_results));
+        foreach ($movies->results as $key => $movie) {
+            fwrite(STDERR, sprintf("[%2d] %s\n", $key + ($page -1) * $itemsPerPage, $movie->title));
+        }
+        if ($page > 1) {
+            fwrite(STDERR, "p: previous page\n");
+        }
+        if ($movies->total_pages > $page) {
+            $itemsPerPage = count($movies->results);
+            fwrite(STDERR, "n: next page\n");
+        }
+        fwrite(STDERR, "\n");
+        fwrite(STDERR, 'Your selection: ');
+        $cmd = readline();
+        if (is_numeric($cmd)) {
+            $num = $cmd - ($page - 1) * $itemsPerPage;
+            if (isset($movies->results[$num])) {
+                $movie = $movies->results[$num];
+                break;
+            }
+            fwrite(STDERR, "Invalid selection $num\n");
+        } else if ($cmd == 'n' && $movies->total_pages > $page) {
+            $page++;
+        } else if ($cmd == 'p' && $page > 1) {
+            $page--;
+        } else if ($cmd == 'q' || $cmd == 'quit' || $cmd == 'exit') {
+            exit(30);
+        }
+
+        $movies = queryTmdb(
+            '/3/search/movie'
+            . '?query=' . urlencode($title)
+            . '&language=' . urlencode($language)
+            . '&include_adult=1'
+            . '&page=' . $page
+        );
+    } while (true);
+}
+
+
+$details = queryTmdb('3/movie/' . $movie->id . '?language=' . $language);
+$credits = queryTmdb('3/movie/' . $movie->id . '/credits?language=' . $language);
+
+
+$xml = new MkvTagXMLWriter();
+if ($outfile === null) {
+    $xml->openMemory();
+} else {
+    $xml->openURI($outfile);
+}
+$xml->setIndent(true);
+$xml->startDocument("1.0");
+$xml->writeRaw("<!DOCTYPE Tags SYSTEM \"matroskatags.dtd\">\n");
+$xml->startElement("Tags");
+$xml->startElement("Tag");
+
+$xml->targetType(50);
+$xml->simple('TITLE', $movie->title, $language);
+if ($language != $movie->original_language) {
+    $xml->simple('TITLE', $movie->original_title, $movie->original_language);
+}
+$xml->simple('SUBTITLE', $details->tagline, $language);
+$xml->simple('SYNOPSIS', $movie->overview, $language);
+
+$xml->simple('DATE_RELEASED', $movie->release_date);
+
+foreach ($details->genres as $genre) {
+    $xml->simple('GENRE', $genre->name, $language);
+}
+
+$xml->simple('RATING', $movie->vote_average / 2);//0-10 on TMDB, 0-5 mkv
+$xml->simple('TMDB', 'movie/' . $movie->id);
+$xml->simple('IMDB', $details->imdb_id);
+
+foreach ($credits->cast as $actor) {
+    $xml->actor($actor->name, $actor->character, $language);
+}
+
+//map tmdb job to matroska tags
+$crewMap = [
+    'Art Direction'           => 'ART_DIRECTOR',
+    'Costume Design'          => 'COSTUME_DESIGNER',
+    'Director of Photography' => 'DIRECTOR_OF_PHOTOGRAPHY',
+    'Director'                => 'DIRECTOR',
+    'Editor'                  => 'EDITED_BY',
+    'Novel'                   => 'WRITTEN_BY',
+    'Original Music Composer' => 'COMPOSER',
+    'Producer'                => 'PRODUCER',
+    'Screenplay'              => 'WRITTEN_BY',
+    'Sound'                   => 'COMPOSER',
+    'Theme Song Performance'  => 'LEAD_PERFORMER',
+];
+foreach ($credits->crew as $crewmate) {
+    if (isset($crewMap[$crewmate->job])) {
+        $xml->simple($crewMap[$crewmate->job], $crewmate->name);
+    }
+}
+
+
+$xml->endElement();//Tag
+$xml->endElement();//Tags
+$xml->endDocument();
+
+if ($outfile === null) {
+    echo $xml->outputMemory();
+} else {
+    $xml->flush();
+}
+
+
+//var_dump($credits);
+//var_dump($movie, $details);
+
+
+//$tmdbConfig = queryTmdb('3/configuration');
+
+
+function queryTmdb($path)
+{
+    global $apiToken;
+
+    $url = 'https://api.themoviedb.org/' . $path;
+    $ctx = stream_context_create(
+        [
+            'http' => [
+                'timeout'       => 5,
+                'ignore_errors' => true,
+                'header'        => 'Authorization: Bearer ' . $apiToken
+            ]
+        ]
+    );
+    $res = file_get_contents($url, false, $ctx);
+    list(, $statusCode) = explode(' ', $http_response_header[0]);
+    $data = json_decode($res);
+
+    if ($statusCode != 200) {
+        if (isset($data->status_code) && isset($data->status_message)) {
+            throw new Exception(
+                'API error: ' . $data->status_code . ' ' . $data->status_message
+            );
+        }
+        throw new Exception('Error querying API: ' . $statusCode, $statusCode);
+    }
+    return $data;
+}
+
+class MkvTagXMLWriter extends XMLWriter
+{
+    public function actor($actorName, $characterName)
+    {
+        $this->startElement('Simple');
+        $this->startElement('Name');
+        $this->text('ACTOR');
+        $this->endElement();
+        $this->startElement('String');
+        $this->text($actorName);
+        $this->endElement();
+        $this->simple('CHARACTER', $characterName);
+        $this->endElement();//Simple
+    }
+
+    public function simple($key, $value, $language = null)
+    {
+        $this->startElement('Simple');
+        $this->startElement('Name');
+        $this->text($key);
+        $this->endElement();
+        $this->startElement('String');
+        $this->text($value);
+        $this->endElement();
+        if ($language) {
+            $this->startElement('TagLanguage');
+            $this->text($language);
+            $this->endElement();
+        }
+        $this->endElement();
+    }
+
+    public function targetType($value)
+    {
+        $this->startElement('Targets');
+        $this->startElement('TargetType');
+        $this->text($value);
+        $this->endElement();
+        $this->endElement();
+    }
+}
+?>