Passed
Push — master ( b191fa...ec9cdb )
by Pauli
04:01
created

DetailsHelper::transformLyrics()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 3
nop 1
dl 0
loc 20
ccs 0
cts 13
cp 0
crap 42
rs 9.2222
c 0
b 0
f 0
1
<?php
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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2018 - 2020
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use \OCA\Music\AppFramework\Core\Logger;
16
17
18
19
class DetailsHelper {
20
	private $extractor;
21
	private $logger;
22
23
	public function __construct(
24
			Extractor $extractor,
25
			Logger $logger) {
26
		$this->extractor = $extractor;
27
		$this->logger = $logger;
28
	}
29
30
	/**
31
	 * @param integer $fileId
32
	 * @param Folder $userFolder
0 ignored issues
show
Bug introduced by
The type OCA\Music\Utility\Folder was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
	 * $return array|null
34
	 */
35
	public function getDetails($fileId, $userFolder) {
36
		$fileNodes = $userFolder->getById($fileId);
37
		if (\count($fileNodes) > 0) {
38
			$data = $this->extractor->extract($fileNodes[0]);
39
40
			// remove intermediate arrays and map null comments into an empty array
41
			$data['comments'] = self::flattenComments($data['comments'] ?: []);
42
43
			// cleanup strings from invalid characters
44
			\array_walk($data['audio'], ['self', 'sanitizeString']);
45
			\array_walk($data['comments'], ['self', 'sanitizeString']);
46
47
			$result = [
48
				'fileinfo' => $data['audio'],
49
				'tags' => $data['comments']
50
			];
51
52
			// binary data has to be encoded
53
			if (\array_key_exists('picture', $result['tags'])) {
54
				$result['tags']['picture'] = self::encodePictureTag($result['tags']['picture']);
55
			}
56
57
			// 'streams' contains duplicate data
58
			unset($result['fileinfo']['streams']);
59
60
			// one track number is enough
61
			if (\array_key_exists('track', $result['tags'])
62
				&& \array_key_exists('track_number', $result['tags'])) {
63
				unset($result['tags']['track']);
64
			}
65
66
			// special handling for lyrics tags
67
			$lyricsNode = self::transformLyrics($result['tags']);
68
			if ($lyricsNode !== null) {
69
				$result['lyrics'] = $lyricsNode;
70
				unset($result['tags']['LYRICS']);
71
				unset($result['tags']['unsynchronised_lyric']);
72
				unset($result['tags']['unsynced lyrics']);
73
			}
74
75
			// add track length
76
			if (\array_key_exists('playtime_seconds', $data)) {
77
				$result['length'] = \ceil($data['playtime_seconds']);
78
			} else {
79
				$result['length'] = null;
80
			}
81
82
			// add file path
83
			$result['path'] = $userFolder->getRelativePath($fileNodes[0]->getPath());
84
85
			return $result;
86
		}
87
		return null;
88
	}
89
90
	/**
91
	 * @param integer $fileId
92
	 * @param Folder $userFolder
93
	 * $return string|null
94
	 */
95
	public function getLyrics($fileId, $userFolder) {
96
		$lyrics = null;
97
		$fileNodes = $userFolder->getById($fileId);
98
		if (\count($fileNodes) > 0) {
99
			$data = $this->extractor->extract($fileNodes[0]);
100
			$lyrics = ExtractorGetID3::getFirstOfTags($data, ['unsynchronised_lyric', 'unsynced lyrics']);
101
102
			if ($lyrics === null) {
103
				// no unsynchronized lyrics, try to get and convert the potentially syncronized lyrics
104
				$lyrics = ExtractorGetID3::getTag($data, 'LYRICS');
105
				$parsed = LyricsParser::parseSyncedLyrics($lyrics, $this->logger);
0 ignored issues
show
Unused Code introduced by
The call to OCA\Music\Utility\Lyrics...er::parseSyncedLyrics() has too many arguments starting with $this->logger. ( Ignorable by Annotation )

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

105
				/** @scrutinizer ignore-call */ 
106
    $parsed = LyricsParser::parseSyncedLyrics($lyrics, $this->logger);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
106
				if ($parsed) {
107
					// the lyrics were indeed time-synced, convert the parsed array to a plain string
108
					$lyrics = LyricsParser::syncedToUnsynced($parsed);
109
				}
110
			}
111
		}
112
		return $lyrics;
113
	}
114
115
	/**
116
	 * Read lyrics-related tags, and build a result array containing potentially
117
	 * both time-synced and unsynced lyrics. If no lyrics tags are found, the result will
118
	 * be null. In case the result is non-null, there is always at least the key 'unsynced'
119
	 * in the result which will hold a string representing the lyrics with no timestamps.
120
	 * If found and successfully parsed, there will be also another key 'synced', which will
121
	 * hold the time-synced lyrics. These are presented as an associative array where the
122
	 * keys are timestamps and values are corresponding lines of text.
123
	 * 
124
	 * @param array $tags
125
	 * @return array|null
126
	 */
127
	private static function transformLyrics($tags) {
128
		$lyrics = Util::arrayGetOrDefault($tags, 'LYRICS'); // may be synced or unsynced
129
		$syncedLyrics = LyricsParser::parseSyncedLyrics($lyrics);
130
		$unsyncedLyrics = Util::arrayGetOrDefault($tags, 'unsynchronised_lyric')
131
						?: Util::arrayGetOrDefault($tags, 'unsynced lyrics')
132
						?: LyricsParser::syncedToUnsynced($syncedLyrics)
133
						?: $lyrics;
134
135
		if ($unsyncedLyrics !== null) {
136
			$result = ['unsynced' => $unsyncedLyrics];
137
138
			if ($syncedLyrics !== null) {
139
				$result['synced'] = $syncedLyrics;
140
			}
141
		}
142
		else {
143
			$result = null;
144
		}
145
146
		return $result;
147
	}
148
149
	/**
150
	 * Base64 encode the picture binary data and wrap it so that it can be directly used as
151
	 * src of an HTML img element.
152
	 * @param string $pic
153
	 * @return string|null
154
	 */
155
	private static function encodePictureTag($pic) {
156
		if ($pic['data']) {
157
			return 'data:' . $pic['image_mime'] . ';base64,' . \base64_encode($pic['data']);
158
		} else {
159
			return null;
160
		}
161
	}
162
163
	/**
164
	 * Remove potentially invalid characters from the string.
165
	 * @param $item
166
	 */
167
	private static function sanitizeString(&$item){
168
		if (is_string($item)) {
169
			$item = \mb_convert_encoding($item, 'UTF-8', 'UTF-8');
170
		}
171
	}
172
173
	/**
174
	 * In the 'comments' field from the extractor, the value for each key is a 1-element
175
	 * array containing the actual tag value. Remove these intermediate arrays.
176
	 * @param array $array
177
	 * @return array
178
	 */
179
	private static function flattenComments($array) {
180
		// key 'text' is an exception, its value is an associative array
181
		$textArray = null;
182
183
		foreach ($array as $key => $value) {
184
			if ($key === 'text') {
185
				$textArray = $value;
186
			} else {
187
				$array[$key] = $value[0];
188
			}
189
		}
190
191
		if (!empty($textArray)) {
192
			$array = \array_merge($array, $textArray);
193
			unset($array['text']);
194
		} 
195
196
		return $array;
197
	}
198
199
}
200