isEnabled()) { $this->error('NAS is not reachable — cannot repair. Try again when NAS is back online.'); return self::FAILURE; } $targetArg = $this->argument('target'); $target = is_numeric($targetArg) ? Video::find((int) $targetArg) : Video::find(Video::decodeId((string) $targetArg)); if (!$target) { $this->error("Target video not found: {$targetArg}"); return self::FAILURE; } $this->info("Target: [{$target->id}] {$target->title}"); $redirects = DB::table('video_redirects')->where('target_id', $target->id); if ($sid = $this->option('source-id')) { $redirects->where('source_id', (int) $sid); } $redirects = $redirects->get(); if ($redirects->isEmpty()) { $this->warn('No video_redirects rows found for this target.'); return self::SUCCESS; } $dryRun = (bool) $this->option('dry-run'); $adopted = 0; $folderOverride = $this->option('source-folder'); foreach ($redirects as $r) { $this->line(''); $sourceFolder = $folderOverride ?: $r->source_folder; $this->info("Source id {$r->source_id} → folder: " . ($sourceFolder ?: '(unknown)')); if (!$sourceFolder) { $this->warn(' Skipping — no source_folder recorded. Pass --source-folder="users/…/music/…" to specify.'); continue; } // Backfill the redirect row so future runs don't need the override. if (!$r->source_folder && !$dryRun && $folderOverride) { DB::table('video_redirects')->where('source_id', $r->source_id) ->update(['source_folder' => $folderOverride]); $r->source_folder = $folderOverride; } else { $r->source_folder = $sourceFolder; } $tracksDir = $r->source_folder . '/tracks'; $subdirs = $nas->listNasDirs($tracksDir); if (empty($subdirs)) { $this->line(' No leftover track subfolders. Cleaning up empty source folder.'); if (!$dryRun) { $nas->deleteFile($r->source_folder . '/meta.json'); $nas->deleteFolder($tracksDir); $nas->deleteFolder($r->source_folder); } continue; } $targetDir = $nas->resolveVideoDir($target); $nas->mkdirp("{$targetDir}/tracks"); foreach ($subdirs as $sub) { // Parse "{lang}-{origId}" to recover the language for the new track. $lang = 'xx'; if (preg_match('/^([a-z]{2,10})-\d+$/i', $sub, $m)) { $lang = strtolower($m[1]); } $this->line(" Adopting track folder: {$sub} (lang={$lang})"); if ($dryRun) { $adopted++; continue; } $newTrack = VideoAudioTrack::create([ 'video_id' => $target->id, 'language' => $lang, 'label' => "Recovered from merge #{$r->source_id}", 'title' => null, 'filename' => 'audio.mp3', 'path' => '', ]); $newBasename = $nas->trackFolderName($target, $newTrack); $oldRel = "{$tracksDir}/{$sub}"; $newRel = "{$targetDir}/tracks/{$newBasename}"; $nas->renameNasPath($oldRel, $newRel); // Local mirror (if the folder happens to exist locally too). $oldLocal = storage_path('app/' . $oldRel); $newLocal = storage_path('app/' . $newRel); if (is_dir($oldLocal)) { @mkdir(dirname($newLocal), 0755, true); @rename($oldLocal, $newLocal); } // Detect audio filename from local mirror if present, else default. $audioName = 'audio.mp3'; foreach (['audio.mp3', 'audio.m4a', 'audio.aac', 'audio.ogg', 'audio.wav', 'audio.opus'] as $cand) { if (file_exists("{$newLocal}/{$cand}")) { $audioName = $cand; break; } } $newTrack->update([ 'filename' => $audioName, 'path' => "{$newRel}/{$audioName}", ]); $adopted++; } // Clean up now-empty source folder. if (!$dryRun) { $nas->deleteFile($r->source_folder . '/meta.json'); $nas->deleteFolder($tracksDir); $nas->deleteFolder($r->source_folder); } } $this->line(''); $this->info(($dryRun ? '[dry-run] Would adopt ' : 'Adopted ') . $adopted . ' track folder(s).'); return self::SUCCESS; } }