ExtractorGetID3::extract()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 17
rs 9.9332
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 $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() : void {
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->error("Exception/Error $eClass when analyzing file {$file->getPath()}\n"
72
						. "Message: {$e->getMessage()}, Stack trace: {$e->getTraceAsString()}");
73
		}
74
75
		return $metadata;
76
	}
77
78
	private function doExtract(File $file) : array {
79
		\assert($this->getID3 !== null, 'initGetID3 must be called first');
80
		/** @var ?resource $fp */ // null value has been seen at least on some cloud versions although phpdoc of File::fopen doesn't allow it
81
		$fp = $file->fopen('r');
82
83
		if (empty($fp)) {
84
			// note: some of the file opening errors throw and others return a null fp
85
			$this->logger->error("Failed to open file {$file->getPath()} for metadata extraction");
86
			$metadata = [];
87
		} else {
88
			\mb_substitute_character(0x3F);
89
			$metadata = $this->getID3->analyze($file->getPath(), $file->getSize(), '', $fp);
0 ignored issues
show
Bug introduced by
It seems like $file->getSize() can also be of type double; however, parameter $filesize of getID3::analyze() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

89
			$metadata = $this->getID3->analyze($file->getPath(), /** @scrutinizer ignore-type */ $file->getSize(), '', $fp);
Loading history...
90
91
			$this->getID3->CopyTagsToComments($metadata);
92
93
			if (\array_key_exists('error', $metadata)) {
94
				foreach ($metadata['error'] as $error) {
95
					$this->logger->debug('getID3 error occurred');
96
					// sometimes $error is string but can't be concatenated to another string and weirdly just hide the log message
97
					$this->logger->debug('getID3 error message: '. $error);
98
				}
99
			}
100
		}
101
102
		return $metadata;
103
	}
104
105
	/**
106
	 * extract embedded cover art image from media file
107
	 *
108
	 * @param File $file the media file
109
	 * @return ?array{image_mime: string, data: string}
110
	 */
111
	public function parseEmbeddedCoverArt(File $file) : ?array {
112
		$fileInfo = $this->extract($file);
113
		$pic = self::getTag($fileInfo, 'picture', true);
114
		\assert($pic === null || \is_array($pic));
115
		return $pic;
116
	}
117
118
	/**
119
	 * @param array $fileInfo
120
	 * @param string $tag
121
	 * @param bool $binaryValued
122
	 * @return string|int|array|null
123
	 */
124
	public static function getTag(array $fileInfo, string $tag, bool $binaryValued = false) {
125
		$value = $fileInfo['comments'][$tag][0]
126
				?? $fileInfo['comments']['text'][$tag]
127
				?? null;
128
129
		if (\is_string($value) && !$binaryValued) {
130
			// Ensure that the tag contains only valid utf-8 characters.
131
			// Illegal characters may result, if the file metadata has a mismatch
132
			// between claimed and actual encoding. Invalid characters could break
133
			// the database update.
134
			\mb_substitute_character(0xFFFD); // Use the Unicode REPLACEMENT CHARACTER (U+FFFD)
135
			$value = \mb_convert_encoding($value, 'UTF-8', 'UTF-8');
136
		}
137
138
		return $value;
139
	}
140
141
	/**
142
	 * @param array $fileInfo
143
	 * @param string[] $tags
144
	 * @param string|array|null $defaultValue
145
	 * @return string|int|array|null
146
	 */
147
	public static function getFirstOfTags(array $fileInfo, array $tags, $defaultValue = null) {
148
		foreach ($tags as $tag) {
149
			$value = self::getTag($fileInfo, $tag);
150
			if ($value !== null && $value !== '') {
151
				return $value;
152
			}
153
		}
154
		return $defaultValue;
155
	}
156
157
	/**
158
	 * Given an array of tag names, return an associative array of those
159
	 * tag names and values which can be found.
160
	 */
161
	public static function getTags(array $fileInfo, array $tags) : array {
162
		$result = [];
163
		foreach ($tags as $tag) {
164
			$value = self::getTag($fileInfo, $tag);
165
			if ($value !== null && $value !== '') {
166
				$result[$tag] = $value;
167
			}
168
		}
169
		return $result;
170
	}
171
}
172