owncloud /
music
| 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
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 |