Video merge feature, default avatar, video-redirects table, misc UI tweaks
Bundled pre-existing changes on the branch: - Video merge: controller, service, admin merge modal, and a repair console command; new video_redirects table (with source_folder) so merged/moved videos keep resolving under their old keys. - Default avatar SVG for users without a profile picture. - Assorted view tweaks across profile, video actions/comments/insights, admin layout, video-details, and the app layout. - Small touch-ups in SuperAdminController, User model, NasSyncService. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
ba8ab11ce4
commit
8d27520bdb
17 files changed
+736
-13
No files matched your search
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Video;
|
||||
use App\Models\VideoAudioTrack;
|
||||
use App\Services\NasSyncService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RepairMergedVideo extends Command
|
||||
{
|
||||
protected $signature = 'videos:repair-merged {target : Target video encoded id (from the URL) or numeric id} {--source-id= : Force a specific source id from video_redirects} {--source-folder= : Override the source folder path (use when the redirect row has none — pre-repair-column merges)} {--dry-run : Show what would happen, do not modify anything}';
|
||||
|
||||
protected $description = 'After a music-video merge, adopt any track subfolders still stuck in the source folder on NAS as new audio tracks on the target video. Safe to re-run.';
|
||||
|
||||
public function handle(NasSyncService $nas): int
|
||||
{
|
||||
if (!$nas->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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user