- POST /api/ccli/preview: parse-only endpoint (no DB writes) - POST /api/songs/import-from-ccli-paste: 3 modes (create / pair-with-song / assign-to-service-song) - GET /songs/import-from-ccli-paste: Inertia page with base64 bookmarklet prefill - Routes guarded by auth:sanctum + throttle:30,1 (API); auth + web stack (web) - Maps DuplicateCcliSongException to 409 with existing_song_id and edit_url - Pest tests (10 cases, 63 assertions): preview, all 3 import modes, 409 dup, 422 errors, unauth, prefill happy/error, login redirect
178 lines
6.4 KiB
PHP
178 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Exceptions\DuplicateCcliSongException;
|
|
use App\Models\ServiceSong;
|
|
use App\Models\Song;
|
|
use App\Services\CcliImportService;
|
|
use App\Services\CcliPasteParser;
|
|
use App\Services\CcliTranslationPairingService;
|
|
use App\Services\SongMatchingService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response as InertiaResponse;
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
final class CcliPasteController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly CcliPasteParser $parser,
|
|
private readonly CcliImportService $importService,
|
|
private readonly CcliTranslationPairingService $pairingService,
|
|
private readonly SongMatchingService $matchingService,
|
|
) {}
|
|
|
|
/**
|
|
* POST /api/ccli/preview
|
|
* Parse raw text and return DTO as JSON. No DB writes.
|
|
*/
|
|
public function preview(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'raw_text' => ['required', 'string', 'max:200000'],
|
|
]);
|
|
|
|
try {
|
|
$parsed = $this->parser->parse($validated['raw_text']);
|
|
} catch (InvalidArgumentException $e) {
|
|
return response()->json([
|
|
'message' => $e->getMessage(),
|
|
], 422);
|
|
}
|
|
|
|
return response()->json([
|
|
'title' => $parsed->title,
|
|
'author' => $parsed->author,
|
|
'ccliId' => $parsed->ccliId,
|
|
'year' => $parsed->year,
|
|
'copyrightText' => $parsed->copyrightText,
|
|
'sections' => array_map(fn ($s) => [
|
|
'label' => $s->label,
|
|
'kind' => $s->kind,
|
|
'number' => $s->number,
|
|
'modifier' => $s->modifier,
|
|
'lines' => $s->lines,
|
|
'hasTranslation' => $s->linesTranslated !== null,
|
|
], $parsed->sections),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* POST /api/songs/import-from-ccli-paste
|
|
* Import a CCLI paste in one of 3 modes.
|
|
*/
|
|
public function importPaste(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'raw_text' => ['required', 'string', 'max:200000'],
|
|
'mode' => ['required', Rule::in(['create', 'pair-with-song', 'assign-to-service-song'])],
|
|
'target_id' => ['required_unless:mode,create', 'nullable', 'integer'],
|
|
'source_url' => ['nullable', 'string', 'max:500'],
|
|
]);
|
|
|
|
$rawText = $validated['raw_text'];
|
|
$mode = $validated['mode'];
|
|
$targetId = $validated['target_id'] ?? null;
|
|
$sourceUrl = $validated['source_url'] ?? null;
|
|
|
|
try {
|
|
if ($mode === 'create') {
|
|
$result = $this->importService->import($rawText, $sourceUrl);
|
|
|
|
return response()->json([
|
|
'song_id' => $result['song']->id,
|
|
'status' => $result['status'],
|
|
'warnings' => $result['warnings'],
|
|
], 201);
|
|
}
|
|
|
|
if ($mode === 'pair-with-song') {
|
|
$localSong = Song::findOrFail($targetId);
|
|
$result = $this->pairingService->pair($localSong, $rawText);
|
|
|
|
session()->flash('ccli_prefilled', $result['distributed_text']);
|
|
|
|
return response()->json([
|
|
'song_id' => $localSong->id,
|
|
'mapping' => $result['mapping'],
|
|
'unmatched_labels' => $result['unmatched_labels'],
|
|
'redirect_to' => route('songs.translate', $localSong->id).'?prefilled=true',
|
|
]);
|
|
}
|
|
|
|
if ($mode === 'assign-to-service-song') {
|
|
$result = $this->importService->import($rawText, $sourceUrl);
|
|
$song = $result['song'];
|
|
|
|
$serviceSong = ServiceSong::findOrFail($targetId);
|
|
$this->matchingService->manualAssign($serviceSong, $song);
|
|
|
|
return response()->json([
|
|
'song_id' => $song->id,
|
|
'service_song_id' => $serviceSong->id,
|
|
'status' => $result['status'],
|
|
], 201);
|
|
}
|
|
} catch (DuplicateCcliSongException $e) {
|
|
return response()->json([
|
|
'message' => $e->getMessage(),
|
|
'existing_song_id' => $e->existingSongId,
|
|
'edit_url' => route('songs.index').'#song-'.$e->existingSongId,
|
|
], 409);
|
|
} catch (InvalidArgumentException $e) {
|
|
return response()->json(['message' => $e->getMessage()], 422);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['message' => 'Unbekannter Modus'], 422);
|
|
}
|
|
|
|
/**
|
|
* GET /songs/import-from-ccli-paste
|
|
* Render the Inertia page for bookmarklet redirect.
|
|
*/
|
|
public function showImportPage(Request $request): InertiaResponse
|
|
{
|
|
$prefill = $request->query('prefill');
|
|
$prefilledText = null;
|
|
$prefilledMetadata = null;
|
|
$prefillError = null;
|
|
|
|
if ($prefill !== null && is_string($prefill)) {
|
|
try {
|
|
$decoded = base64_decode($prefill, strict: true);
|
|
if ($decoded === false) {
|
|
throw new InvalidArgumentException('Ungültige Kodierung');
|
|
}
|
|
$payload = json_decode($decoded, associative: true, flags: JSON_THROW_ON_ERROR);
|
|
|
|
if (! is_array($payload)) {
|
|
throw new InvalidArgumentException('Ungültiges Payload-Format');
|
|
}
|
|
|
|
$prefilledText = $payload['rawText'] ?? null;
|
|
$prefilledMetadata = [
|
|
'title' => $payload['title'] ?? null,
|
|
'author' => $payload['author'] ?? null,
|
|
'ccliId' => $payload['ccliId'] ?? null,
|
|
'sourceUrl' => $payload['sourceUrl'] ?? null,
|
|
];
|
|
} catch (Throwable) {
|
|
$prefillError = 'Lesezeichen-Daten konnten nicht gelesen werden. Bitte den Liedtext manuell einfügen.';
|
|
}
|
|
}
|
|
|
|
return Inertia::render('Songs/ImportFromCcliPaste', [
|
|
'prefilledText' => $prefilledText,
|
|
'prefilledMetadata' => $prefilledMetadata,
|
|
'prefillError' => $prefillError,
|
|
]);
|
|
}
|
|
}
|