Issues (40)

lib/Service/LyricsParser.php (1 issue)

Severity
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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2020 - 2025
11
 */
12
13
namespace OCA\Music\Service;
14
15
class LyricsParser {
16
17
	/**
18
	 * Take the timestamped lyrics as returned by `LyricsParser::parseSyncedLyrics` and
19
	 * return the corresponding plain text representation with no LRC tags.
20
	 * Input value null will give null result.
21
	 *
22
	 * @param array|null $parsedSyncedLyrics
23
	 * @return string|null
24
	 */
25
	public static function syncedToUnsynced(?array $parsedSyncedLyrics) {
26
		return ($parsedSyncedLyrics === null) ? null : \implode("\n", $parsedSyncedLyrics);
0 ignored issues
show
The condition $parsedSyncedLyrics === null is always false.
Loading history...
27
	}
28
29
	/**
30
	 * Parse timestamped lyrics from the given string, and return the parsed data.
31
	 * Return null if the string does not appear to be timestamped lyric in the LRC format.
32
	 *
33
	 * @return array|null The keys of the array are timestamps in milliseconds and values are
34
	 *                    corresponding lines of lyrics.
35
	 */
36
	public static function parseSyncedLyrics(?string $data) : ?array {
37
		$parsedLyrics = [];
38
39
		if (!empty($data)) {
40
			$offset = 0;
41
42
			$fp = \fopen("php://temp", 'r+');
43
			\assert($fp !== false, 'Unexpected error: opening temporary stream failed');
44
45
			\fputs($fp, $data);
46
			\rewind($fp);
47
			while ($line = \fgets($fp)) {
48
				$lineParseResult = self::parseTimestampedLrcLine($line, $offset);
49
				$parsedLyrics += $lineParseResult;
50
			}
51
			\fclose($fp);
52
53
			// sort the parsed lyric lines according the timestamps (which are keys of the array)
54
			\ksort($parsedLyrics);
55
		}
56
57
		return \count($parsedLyrics) > 0 ? $parsedLyrics : null;
58
	}
59
60
	/**
61
	 * Parse a single line of LRC formatted data. The result is array where keys are
62
	 * timestamps and values are corresponding lyrics. A single line of LRC may span out to
63
	 * a) 0 actual timestamp lines, if the line is empty, or contains just metadata, or contains no tags
64
	 * b) 1 actual timestamp line (the "normal" case)
65
	 * c) several actual timestamp lines, in case the line contains several timestamps,
66
	 *    meaning that the same line of text is repeated multiple times during the song
67
	 *
68
	 * If the line defines a time offset, this is returned in the reference parameter. If the offset
69
	 * parameter holds a non-zero value on call, the offset is applied on any extracted timestamps.
70
	 *
71
	 * @param string $line One line from the LRC data
72
	 * @param int $offset Input/output value for time offset in milliseconds
73
	 * @return array
74
	 */
75
	private static function parseTimestampedLrcLine(string $line, int &$offset) : array {
76
		$result = [];
77
		$line = \trim($line);
78
79
		$matches = [];
80
		if (\preg_match('/(\[.+\])(.*)/', $line, $matches)) {
81
			// 1st group captures tag(s), 2nd group anything after the tag(s).
82
			$tags = $matches[1];
83
			$text = $matches[2];
84
85
			// Extract timestamp tags and the offset tag and discard any other metadata tags.
86
			$timestampMatches = [];
87
			$offsetMatch = [];
88
			if (\preg_match('/\[offset:(\d+)\]/', $tags, $offsetMatch)) {
89
				$offset = \intval($offsetMatch[1]);
90
			} elseif (\preg_match_all('/\[(\d\d:\d\d(\.\d\d)?)\]/', $tags, $timestampMatches)) {
91
				// some timestamp(s) were found
92
				$timestamps = $timestampMatches[1];
93
94
				// add the line text to the result set on each found timestamp
95
				foreach ($timestamps as $timestamp) {
96
					$result[self::timestampToMs($timestamp) - $offset] = $text;
97
				}
98
			}
99
		}
100
101
		return $result;
102
	}
103
104
	/**
105
	 * Convert timestamp in "mm:ss.ff" format to milliseconds
106
	 */
107
	private static function timestampToMs(string $timestamp) : int {
108
		list($minutes, $seconds) = \sscanf($timestamp, "%d:%f");
109
		return \intval($seconds * 1000 + $minutes * 60 * 1000);
110
	}
111
}
112