feat: read LRC files if applicable (closes #1447) (#1502)

This commit is contained in:
Phan An 2022-09-14 19:12:06 +07:00 committed by GitHub
parent f3e113e8a8
commit 4306d1e6f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 118 additions and 14 deletions

View file

@ -3,25 +3,19 @@ on:
pull_request:
branches:
- master
- next
paths:
- ./*
- '!resources/assets/**'
- .github/workflows/unit-backend.yml
push:
branches:
- master
- next
paths:
- ./*
- '!resources/assets/**'
- .github/workflows/unit-backend.yml
workflow_dispatch:
branches:
- master
- next
paths:
- ./*
- '!resources/assets/**'
- .github/workflows/unit-backend.yml
jobs:

View file

@ -3,7 +3,6 @@ on:
pull_request:
branches:
- master
- next
paths:
- resources/assets/**
- .github/workflows/unit-frontend.yml

View file

@ -5,7 +5,7 @@ namespace App\Http\Requests\API;
use App\Rules\ValidSmartPlaylistRulePayload;
/**
* @property-read $name
* @property-read string $name
* @property-read array $rules
*/
class PlaylistUpdateRequest extends Request

View file

@ -98,8 +98,13 @@ class Song extends Model
protected function lyrics(): Attribute
{
// Since we're displaying the lyrics using <pre>, replace breaks with newlines and strip all tags.
$normalizer = static fn (?string $value): string => strip_tags(preg_replace('#<br\s*/?>#i', PHP_EOL, $value));
$normalizer = static function (?string $value): string {
// Since we're displaying the lyrics using <pre>, replace breaks with newlines and strip all tags.
$value = strip_tags(preg_replace('#<br\s*/?>#i', PHP_EOL, $value));
// also remove the timestamps that often come with LRC files
return preg_replace('/\[\d{2}:\d{2}.\d{2}]\s*/m', '', $value);
};
return new Attribute(get: $normalizer, set: $normalizer);
}

View file

@ -30,6 +30,7 @@ class FileSynchronizer
private getID3 $getID3,
private MediaMetadataService $mediaMetadataService,
private SongRepository $songRepository,
private SimpleLrcReader $lrcReader,
private Cache $cache,
private Finder $finder
) {
@ -51,7 +52,14 @@ class FileSynchronizer
$info = $this->getID3->analyze($this->filePath);
$this->syncError = Arr::get($info, 'error.0') ?: (Arr::get($info, 'playtime_seconds') ? null : 'Empty file');
return $this->syncError ? null : SongScanInformation::fromGetId3Info($info);
if ($this->syncError) {
return null;
}
$info = SongScanInformation::fromGetId3Info($info);
$info->lyrics = $info->lyrics ?: $this->lrcReader->tryReadForMediaFile($this->filePath);
return $info;
}
/**

View file

@ -0,0 +1,32 @@
<?php
namespace App\Services;
use Throwable;
class SimpleLrcReader
{
public function tryReadForMediaFile(string $mediaFilePath): string
{
$lrcFilePath = self::getLrcFilePath($mediaFilePath);
try {
return $lrcFilePath ? trim(file_get_contents($lrcFilePath)) : '';
} catch (Throwable) {
return '';
}
}
private static function getLrcFilePath(string $mediaFilePath): ?string
{
foreach (['.lrc', '.LRC'] as $extension) {
$lrcFilePath = preg_replace('/\.[^.]+$/', $extension, $mediaFilePath);
if (is_file($lrcFilePath) && is_readable($lrcFilePath)) {
return $lrcFilePath;
}
}
return null;
}
}

View file

@ -17,6 +17,7 @@ const deepMerge = (first: object, second: object) => {
return mergeWith(first, second, (a, b) => {
if (!isObject(b)) return b
// @ts-ignore
return Array.isArray(a) ? [...a, ...b] : { ...a, ...b }
})
}

View file

@ -32,7 +32,7 @@ new class extends UnitTestCase {
}
protected test () {
it('quques and plays', async () => {
it('queues and plays', async () => {
const queueMock = this.mock(queueStore, 'queueIfNotQueued')
const playMock = this.mock(playbackService, 'play')
const song = factory<Song>('song', { playback_state: 'Stopped' })

View file

@ -15,7 +15,7 @@ interface Preferences extends Record<string, any> {
transcodeOnMobile: boolean
supportBarNoBugging: boolean
showAlbumArtOverlay: boolean
theme: Theme['id'] | null
theme?: Theme['id'] | null
}
const preferenceStore = {

View file

@ -3,6 +3,7 @@
namespace Tests\Integration\Services;
use App\Services\FileSynchronizer;
use Illuminate\Support\Str;
use Tests\TestCase;
class FileSynchronizerTest extends TestCase
@ -46,11 +47,34 @@ class FileSynchronizerTest extends TestCase
self::assertEqualsWithDelta(10, $info->length, 0.1);
}
/** @test */
public function testSongWithoutTitleHasFileNameAsTitle(): void
{
$this->fileSynchronizer->setFile(__DIR__ . '/../../songs/blank.mp3');
self::assertSame('blank', $this->fileSynchronizer->getFileScanInformation()->title);
}
public function testIgnoreLrcFileIfEmbeddedLyricsAvailable(): void
{
$base = sys_get_temp_dir() . '/' . Str::uuid();
$mediaFile = $base . '.mp3';
$lrcFile = $base . '.lrc';
copy(__DIR__ . '/../../songs/full.mp3', $mediaFile);
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
self::assertSame("Foo\rbar", $this->fileSynchronizer->setFile($mediaFile)->getFileScanInformation()->lyrics);
}
public function testReadLrcFileIfEmbeddedLyricsNotAvailable(): void
{
$base = sys_get_temp_dir() . '/' . Str::uuid();
$mediaFile = $base . '.mp3';
$lrcFile = $base . '.lrc';
copy(__DIR__ . '/../../songs/blank.mp3', $mediaFile);
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
$info = $this->fileSynchronizer->setFile($mediaFile)->getFileScanInformation();
self::assertSame("Line 1\nLine 2\nLine 3", $info->lyrics);
}
}

View file

@ -14,4 +14,12 @@ class SongTest extends TestCase
self::assertEquals(['bucket' => 'foo', 'key' => 'bar'], $song->s3_params);
}
public function testLyricsDoNotContainTimestamps(): void
{
/** @var Song $song */
$song = Song::factory()->create(['lyrics' => "[00:00.00]Line 1\n[00:01.00]Line 2\n[00:02.00]Line 3"]);
self::assertEquals("Line 1\nLine 2\nLine 3", $song->lyrics);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Tests\Unit\Services;
use App\Services\SimpleLrcReader;
use Illuminate\Support\Str;
use Tests\TestCase;
class SimpleLrcReaderTest extends TestCase
{
private SimpleLrcReader $reader;
public function setUp(): void
{
parent::setUp();
$this->reader = new SimpleLrcReader();
}
public function testTryReadForMediaFile(): void
{
$base = sys_get_temp_dir() . '/' . Str::uuid();
$lrcFile = $base . '.lrc';
copy(__DIR__ . '/../../blobs/simple.lrc', $lrcFile);
self::assertSame("Line 1\nLine 2\nLine 3", $this->reader->tryReadForMediaFile($base . '.mp3'));
@unlink($lrcFile);
}
}

3
tests/blobs/simple.lrc Normal file
View file

@ -0,0 +1,3 @@
Line 1
Line 2
Line 3