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
introduced
by
![]() |
|||
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 |