Passed
Push — master ( f358a5...b5f949 )
by Pauli
03:17
created

ExtractorGetID3::parseEmbeddedCoverArt()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2013, 2014
12
 * @copyright Pauli Järvinen 2016 - 2025
13
 */
14
15
namespace OCA\Music\Service;
16
17
use OCA\Music\AppFramework\Core\Logger;
18
19
use OCP\Files\File;
20
21
/**
22
 * an extractor class for getID3
23
 */
24
class ExtractorGetID3 implements Extractor {
25
	private $getID3;
26
	private Logger $logger;
27
28
	public function __construct(Logger $logger) {
29
		$this->logger = $logger;
30
		$this->getID3 = null; // lazy-loaded
31
	}
32
33
	/**
34
	 * Second stage constructor used to lazy-load the getID3 library once it's needed.
35
	 * This is to prevent polluting the namespace of occ when the user is not running
36
	 * Music app commands.
37
	 * See https://github.com/nextcloud/server/issues/17027.
38
	 */
39
	private function initGetID3() {
40
		if ($this->getID3 === null) {
41
			require_once __DIR__ . '/../../3rdparty/getID3/getid3/getid3.php';
42
			$this->getID3 = new \getID3();
43
			$this->getID3->encoding = 'UTF-8';
44
			$this->getID3->option_tags_html = false; // HTML-encoded tags are not needed
45
			// On 32-bit systems, getid3 tries to make a 2GB size check,
46
			// which does not work with fopen. Disable it.
47
			// Therefore the filesize (determined by getID3) could be wrong
48
			// (for files over ~2 GB) but this isn't used in any way.
49
			$this->getID3->option_max_2gb_check = false;
50
		}
51
	}
52
53
	/**
54
	 * get metadata info for a media file
55
	 *
56
	 * @param File $file the file
57
	 * @return array extracted data
58
	 */
59
	public function extract(File $file) : array {
60
		$this->initGetID3();
61
		$metadata = [];
62
63
		try {
64
			// It would be pointless to try to analyze 0-byte files and it may cause problems when
65
			// the file is stored on a SMB share, see https://github.com/owncloud/music/issues/600
66
			if ($file->getSize() > 0) {
67
				$metadata = $this->doExtract($file);
68
			}
69
		} catch (\Throwable $e) {
70
			$eClass = \get_class($e);
71
			$this->logger->log("Exception/Error $eClass when analyzing file {$file->getPath()}\n"
72
						. "Message: {$e->getMessage()}, Stack trace: {$e->getTraceAsString()}", 'error');
73
		}
74
75
		return $metadata;
76
	}
77
78
	private function doExtract(File $file) : array {
79
		$fp = $file->fopen('r');
80
81
		if (empty($fp)) {
82
			// note: some of the file opening errors throw and others return a null fp
83
			$this->logger->log("Failed to open file {$file->getPath()} for metadata extraction", 'error');
84
			$metadata = [];
85
		} else {
86
			\mb_substitute_character(0x3F);
87
			$metadata = $this->getID3->analyze($file->getPath(), $file->getSize(), '', $fp);
88
89
			$this->getID3->CopyTagsToComments($metadata);
90
91
			if (\array_key_exists('error', $metadata)) {
92
				foreach ($metadata['error'] as $error) {
93
					$this->logger->log('getID3 error occurred', 'debug');
94
					// sometimes $error is string but can't be concatenated to another string and weirdly just hide the log message
95
					$this->logger->log('getID3 error message: '. $error, 'debug');
96
				}
97
			}
98
		}
99
100
		return $metadata;
101
	}
102
103
	/**
104
	 * extract embedded cover art image from media file
105
	 *
106
	 * @param File $file the media file
107
	 * @return array|null Dictionary with keys 'mimetype' and 'content', or null if not found
108
	 */
109
	public function parseEmbeddedCoverArt(File $file) : ?array {
110
		$fileInfo = $this->extract($file);
111
		$pic = self::getTag($fileInfo, 'picture', true);
112
		\assert($pic === null || \is_array($pic));
113
		return $pic;
114
	}
115
116
	/**
117
	 * @param array $fileInfo
118
	 * @param string $tag
119
	 * @param bool $binaryValued
120
	 * @return string|int|array|null
121
	 */
122
	public static function getTag(array $fileInfo, string $tag, bool $binaryValued = false) {
123
		$value = $fileInfo['comments'][$tag][0]
124
				?? $fileInfo['comments']['text'][$tag]
125
				?? null;
126
127
		if (\is_string($value) && !$binaryValued) {
128
			// Ensure that the tag contains only valid utf-8 characters.
129
			// Illegal characters may result, if the file metadata has a mismatch
130
			// between claimed and actual encoding. Invalid characters could break
131
			// the database update.
132
			\mb_substitute_character(0xFFFD); // Use the Unicode REPLACEMENT CHARACTER (U+FFFD)
133
			$value = \mb_convert_encoding($value, 'UTF-8', 'UTF-8');
134
		}
135
136
		return $value;
137
	}
138
139
	/**
140
	 * @param array $fileInfo
141
	 * @param string[] $tags
142
	 * @param string|array|null $defaultValue
143
	 * @return string|int|array|null
144
	 */
145
	public static function getFirstOfTags(array $fileInfo, array $tags, $defaultValue = null) {
146
		foreach ($tags as $tag) {
147
			$value = self::getTag($fileInfo, $tag);
148
			if ($value !== null && $value !== '') {
149
				return $value;
150
			}
151
		}
152
		return $defaultValue;
153
	}
154
155
	/**
156
	 * Given an array of tag names, return an associative array of those
157
	 * tag names and values which can be found.
158
	 */
159
	public static function getTags(array $fileInfo, array $tags) : array {
160
		$result = [];
161
		foreach ($tags as $tag) {
162
			$value = self::getTag($fileInfo, $tag);
163
			if ($value !== null && $value !== '') {
164
				$result[$tag] = $value;
165
			}
166
		}
167
		return $result;
168
	}
169
}
170
171