Replace all SongGroup/SongArrangementGroup model usages with Label/SongArrangementLabel after the schema migration to global labels. Updates 12 app files and 11 test files: - SongService: createDefaultGroups now finds-or-creates global Labels by name - ArrangementController: validates label_id (labels are global, no song-ownership) - ProImportService: imports groups as Labels (firstOrCreate by name); does not overwrite existing label colors per spec - ProExportService/SongPdfController/TranslationService/etc: traverse via arrangements -> arrangementLabels -> label -> songSlides chain - All test factories and assertions adapted to label-based schema
212 lines
6.4 KiB
PHP
212 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Label;
|
|
use App\Models\Song;
|
|
use App\Models\SongArrangement;
|
|
use App\Models\SongArrangementLabel;
|
|
use App\Support\MacroColorConverter;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use ProPresenter\Parser\ProFileReader;
|
|
use ProPresenter\Parser\Song as ProSong;
|
|
use ZipArchive;
|
|
|
|
class ProImportService
|
|
{
|
|
/** @return Song[] */
|
|
public function import(UploadedFile $file): array
|
|
{
|
|
$extension = strtolower($file->getClientOriginalExtension());
|
|
|
|
if ($extension === 'zip') {
|
|
return $this->importZip($file);
|
|
}
|
|
|
|
if ($extension === 'pro') {
|
|
return [$this->importProFile($file->getRealPath())];
|
|
}
|
|
|
|
throw new \InvalidArgumentException('Nur .pro und .zip Dateien sind erlaubt.');
|
|
}
|
|
|
|
/** @return Song[] */
|
|
private function importZip(UploadedFile $file): array
|
|
{
|
|
$zip = new ZipArchive;
|
|
|
|
if ($zip->open($file->getRealPath()) !== true) {
|
|
throw new \RuntimeException('ZIP-Datei konnte nicht geöffnet werden.');
|
|
}
|
|
|
|
$tempDir = sys_get_temp_dir().'/pro-import-'.uniqid();
|
|
mkdir($tempDir, 0755, true);
|
|
|
|
$songs = [];
|
|
|
|
try {
|
|
$zip->extractTo($tempDir);
|
|
$zip->close();
|
|
|
|
$proFiles = glob($tempDir.'/*.pro') ?: [];
|
|
$proFilesNested = glob($tempDir.'/**/*.pro') ?: [];
|
|
$allProFiles = array_unique(array_merge($proFiles, $proFilesNested));
|
|
|
|
if (empty($allProFiles)) {
|
|
throw new \RuntimeException('Keine .pro Dateien im ZIP-Archiv gefunden.');
|
|
}
|
|
|
|
foreach ($allProFiles as $proPath) {
|
|
$songs[] = $this->importProFile($proPath);
|
|
}
|
|
} finally {
|
|
$this->deleteDirectory($tempDir);
|
|
}
|
|
|
|
return $songs;
|
|
}
|
|
|
|
private function importProFile(string $filePath): Song
|
|
{
|
|
$proSong = ProFileReader::read($filePath);
|
|
|
|
return DB::transaction(function () use ($proSong) {
|
|
return $this->upsertSong($proSong);
|
|
});
|
|
}
|
|
|
|
private function upsertSong(ProSong $proSong): Song
|
|
{
|
|
$ccliId = $proSong->getCcliSongNumber();
|
|
|
|
$songData = [
|
|
'title' => $proSong->getName(),
|
|
'author' => $proSong->getCcliAuthor() ?: null,
|
|
'copyright_text' => $proSong->getCcliPublisher() ?: null,
|
|
'copyright_year' => $proSong->getCcliCopyrightYear() ?: null,
|
|
'publisher' => $proSong->getCcliPublisher() ?: null,
|
|
];
|
|
|
|
if ($ccliId) {
|
|
$song = Song::withTrashed()->where('ccli_id', (string) $ccliId)->first();
|
|
|
|
if ($song) {
|
|
if ($song->trashed()) {
|
|
$song->restore();
|
|
}
|
|
$song->update($songData);
|
|
} else {
|
|
$song = Song::create(array_merge($songData, ['ccli_id' => (string) $ccliId]));
|
|
}
|
|
} else {
|
|
$song = Song::create(array_merge($songData, ['ccli_id' => null]));
|
|
}
|
|
|
|
$song->arrangements()->each(function (SongArrangement $arr) {
|
|
$arr->arrangementLabels()->delete();
|
|
});
|
|
$song->arrangements()->delete();
|
|
|
|
$hasTranslation = false;
|
|
$labelsByName = [];
|
|
|
|
foreach ($proSong->getGroups() as $proGroup) {
|
|
$groupName = $proGroup->getName();
|
|
$existingLabel = Label::whereRaw('LOWER(name) = ?', [strtolower($groupName)])->first();
|
|
|
|
if ($existingLabel === null) {
|
|
$color = $proGroup->getColor();
|
|
$hexColor = MacroColorConverter::fromRgba($color) ?? '#808080';
|
|
|
|
$existingLabel = Label::create([
|
|
'name' => $groupName,
|
|
'color' => $hexColor,
|
|
]);
|
|
}
|
|
|
|
$labelsByName[$groupName] = $existingLabel;
|
|
|
|
$existingLabel->songSlides()->delete();
|
|
|
|
foreach ($proSong->getSlidesForGroup($proGroup) as $slidePosition => $proSlide) {
|
|
$translatedText = null;
|
|
|
|
if ($proSlide->hasTranslation()) {
|
|
$translatedText = $proSlide->getTranslation()->getPlainText();
|
|
$hasTranslation = true;
|
|
}
|
|
|
|
$existingLabel->songSlides()->create([
|
|
'order' => $slidePosition,
|
|
'text_content' => $proSlide->getPlainText(),
|
|
'text_content_translated' => $translatedText,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$song->update(['has_translation' => $hasTranslation]);
|
|
|
|
foreach ($proSong->getArrangements() as $proArrangement) {
|
|
$arrangement = $song->arrangements()->create([
|
|
'name' => $proArrangement->getName(),
|
|
'is_default' => strtolower($proArrangement->getName()) === 'normal',
|
|
]);
|
|
|
|
$groupsInArrangement = $proSong->getGroupsForArrangement($proArrangement);
|
|
|
|
foreach ($groupsInArrangement as $order => $proGroup) {
|
|
$label = $labelsByName[$proGroup->getName()] ?? null;
|
|
|
|
if ($label) {
|
|
SongArrangementLabel::create([
|
|
'song_arrangement_id' => $arrangement->id,
|
|
'label_id' => $label->id,
|
|
'order' => $order,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $song->fresh(['arrangements.arrangementLabels.label.songSlides']);
|
|
}
|
|
|
|
public static function rgbaToHex(array $rgba): string
|
|
{
|
|
$r = (int) round(($rgba['r'] ?? 0) * 255);
|
|
$g = (int) round(($rgba['g'] ?? 0) * 255);
|
|
$b = (int) round(($rgba['b'] ?? 0) * 255);
|
|
|
|
return sprintf('#%02X%02X%02X', $r, $g, $b);
|
|
}
|
|
|
|
public static function hexToRgba(string $hex): array
|
|
{
|
|
$hex = ltrim($hex, '#');
|
|
|
|
$r = hexdec(substr($hex, 0, 2)) / 255;
|
|
$g = hexdec(substr($hex, 2, 2)) / 255;
|
|
$b = hexdec(substr($hex, 4, 2)) / 255;
|
|
|
|
return [round($r, 4), round($g, 4), round($b, 4), 1.0];
|
|
}
|
|
|
|
private function deleteDirectory(string $dir): void
|
|
{
|
|
if (! is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$items = scandir($dir);
|
|
foreach ($items as $item) {
|
|
if ($item === '.' || $item === '..') {
|
|
continue;
|
|
}
|
|
|
|
$path = $dir.'/'.$item;
|
|
is_dir($path) ? $this->deleteDirectory($path) : unlink($path);
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
}
|