pp-planer/app/Services/TranslationService.php
Thorsten Bus ae42b48753 feat(songs): per-song sections + section editing; fix CCLI import bugs
Refactor lyric storage so each song owns its sections instead of sharing
global labels. Adds song_sections (per song+label) owning song_slides;
labels stay global ProPresenter group tags (name/color/macro). Arrangements
now reference sections, so editing/importing one song no longer corrupts
others that share a label name.

- New: song_sections table + migration with safe backfill; SongSection,
  SongArrangementSection models; SongSectionController (edit/add/delete
  sections, immediate persistence) wired into SongEditModal.
- Refactor writers/readers: CcliImport, ProImport, SongService,
  ArrangementController, SongController, ProExport, PDF, Translation
  (translation reset now section-scoped), CCLI pairing.
- CCLI import fixes: parse SongSelect copy-icon format (German "Vers"
  abbrev + trailing author), fill empty CTS-synced songs instead of
  blocking as duplicate, distinct label colors per section kind,
  import&edit/existing-song open the edit modal (no 404/405), teleport
  paste dialog above assign dialog, preview shows section content,
  correct SongSelect search URL, copy-icon instructions.
- Bookmarklet clicks #generalCopyLyricsButton and captures clipboard;
  serves correct host from request.
- Export: embed key-visual/background under fixed bundle-relative names.
- Tests updated for the section model; new section + isolation coverage.
2026-05-31 14:45:47 +02:00

88 lines
2.3 KiB
PHP

<?php
namespace App\Services;
use App\Models\Song;
use App\Models\SongSlide;
use Illuminate\Support\Facades\Http;
class TranslationService
{
public function fetchFromUrl(string $url): ?string
{
try {
$response = Http::timeout(10)->get($url);
if ($response->successful()) {
$html = $response->body();
$text = strip_tags($html);
$text = trim($text);
return $text !== '' ? $text : null;
}
} catch (\Exception) {
// Best-effort: Fehler stillschweigend behandeln
}
return null;
}
public function importTranslation(Song $song, string $text): void
{
$translatedLines = explode("\n", $text);
$offset = 0;
$defaultArr = $song->arrangements()
->where('is_default', true)
->with(['arrangementSections' => fn ($q) => $q->orderBy('order'), 'arrangementSections.section.slides'])
->first();
if ($defaultArr === null) {
$this->markAsTranslated($song);
return;
}
foreach ($defaultArr->arrangementSections->sortBy('order') as $arrangementSection) {
$section = $arrangementSection->section;
if ($section === null) {
continue;
}
foreach ($section->slides->sortBy('order') as $slide) {
$originalLineCount = count(explode("\n", $slide->text_content ?? ''));
$chunk = array_slice($translatedLines, $offset, $originalLineCount);
$offset += $originalLineCount;
$slide->update([
'text_content_translated' => implode("\n", $chunk),
]);
}
}
$this->markAsTranslated($song);
}
public function markAsTranslated(Song $song): void
{
$song->update(['has_translation' => true]);
}
public function removeTranslation(Song $song): void
{
$sectionIds = $song->sections()
->pluck('id')
->unique()
->values();
if ($sectionIds->isNotEmpty()) {
SongSlide::whereIn('song_section_id', $sectionIds)->update([
'text_content_translated' => null,
]);
}
$song->update(['has_translation' => false]);
}
}