Align Suno metadata with music library layout

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-01 23:31:11 +09:00
parent 96a54a3eca
commit b769b62b19
4 changed files with 259 additions and 28 deletions
+136 -22
View File
@@ -29,6 +29,16 @@ function Resolve-Ffmpeg {
throw "ffmpeg.exe를 찾지 못했어. -FfmpegPath 로 경로를 지정해줘."
}
function Resolve-Ffprobe {
param([string]$Ffmpeg)
$candidate = Join-Path (Split-Path -Parent $Ffmpeg) "ffprobe.exe"
if (Test-Path -LiteralPath $candidate) { return $candidate }
$cmd = Get-Command ffprobe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
return ""
}
function Resolve-Manifest {
param([string]$Path)
@@ -82,8 +92,7 @@ function Get-TrackFolder {
)
$safeTitle = ConvertTo-SafeName $Title $Id
$shortId = Get-ShortId $Id
return Join-Path $Root ("{0} [{1}]" -f $safeTitle, $shortId)
return Join-Path $Root $safeTitle
}
function Get-AlbumTitle {
@@ -92,7 +101,20 @@ function Get-AlbumTitle {
[string]$Id
)
return "{0} [{1}]" -f (ConvertTo-SafeName $Title $Id), (Get-ShortId $Id)
return ConvertTo-SafeName $Title $Id
}
function Get-NextTrackNumber {
param([string]$Folder)
if (-not (Test-Path -LiteralPath $Folder)) { return 1 }
$numbers = Get-ChildItem -LiteralPath $Folder -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in ".flac", ".mp3", ".m4a" } |
ForEach-Object {
if ($_.BaseName -match '^(\d{2})\s') { [int]$Matches[1] }
}
if (-not $numbers) { return 1 }
return (($numbers | Measure-Object -Maximum).Maximum + 1)
}
function Resolve-LibraryRoot {
@@ -116,7 +138,7 @@ function Test-ExistingTrack {
if (-not $Id -or -not (Test-Path -LiteralPath $Root)) { return $false }
$metadataHit = Get-ChildItem -LiteralPath $Root -Recurse -Filter "*metadata.json" -File -ErrorAction SilentlyContinue |
$metadataHit = Get-ChildItem -LiteralPath $Root -Recurse -Filter "*.json" -File -ErrorAction SilentlyContinue |
Where-Object {
try {
$json = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
@@ -224,7 +246,87 @@ function Add-MetadataArg {
}
}
function Escape-XmlText {
param([string]$Value)
return [System.Security.SecurityElement]::Escape($Value)
}
function Get-AudioDuration {
param(
[string]$File,
[string]$Ffprobe
)
if (-not $Ffprobe -or -not (Test-Path -LiteralPath $File)) {
return [pscustomobject]@{ Seconds = 0; Text = "" }
}
try {
$raw = & $Ffprobe -v quiet -print_format json -show_format $File
$json = $raw | ConvertFrom-Json
$seconds = [double]$json.format.duration
$span = [TimeSpan]::FromSeconds([Math]::Round($seconds))
return [pscustomobject]@{
Seconds = $seconds
Text = "{0:D2}:{1:D2}" -f [int]$span.TotalMinutes, $span.Seconds
}
} catch {
return [pscustomobject]@{ Seconds = 0; Text = "" }
}
}
function Write-AlbumNfo {
param(
[string]$Folder,
[string]$AlbumTitle,
[string]$Artist,
[string]$Genre,
[string]$Ffprobe
)
$tracks = Get-ChildItem -LiteralPath $Folder -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in ".flac", ".mp3", ".m4a" } |
Sort-Object Name
$trackXml = New-Object System.Collections.Generic.List[string]
$totalSeconds = 0
foreach ($file in $tracks) {
$position = 1
$title = [IO.Path]::GetFileNameWithoutExtension($file.Name)
if ($title -match '^(\d{2})\s+(.+)$') {
$position = [int]$Matches[1]
$title = $Matches[2]
}
$duration = Get-AudioDuration $file.FullName $Ffprobe
$totalSeconds += $duration.Seconds
$trackXml.Add(" <track>")
$trackXml.Add(" <position>$position</position>")
$trackXml.Add(" <title>$(Escape-XmlText $title)</title>")
if ($duration.Text) { $trackXml.Add(" <duration>$($duration.Text)</duration>") }
$trackXml.Add(" </track>")
}
$runtime = if ($totalSeconds -gt 0) { [Math]::Ceiling($totalSeconds / 60) } else { 0 }
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('<?xml version="1.0" encoding="utf-8" standalone="yes"?>')
$lines.Add('<album>')
$lines.Add(' <review />')
$lines.Add(' <outline />')
$lines.Add(' <lockdata>false</lockdata>')
$lines.Add(" <dateadded>$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</dateadded>")
$lines.Add(" <title>$(Escape-XmlText $AlbumTitle)</title>")
if ($runtime -gt 0) { $lines.Add(" <runtime>$runtime</runtime>") }
if ($Genre) { $lines.Add(" <genre>$(Escape-XmlText $Genre)</genre>") }
$lines.Add(" <artist>$(Escape-XmlText $Artist)</artist>")
$lines.Add(" <albumartist>$(Escape-XmlText $Artist)</albumartist>")
foreach ($line in $trackXml) { $lines.Add($line) }
$lines.Add('</album>')
$lines | Set-Content -LiteralPath (Join-Path $Folder "album.nfo") -Encoding UTF8
}
$ffmpeg = Resolve-Ffmpeg $FfmpegPath
$ffprobe = Resolve-Ffprobe $ffmpeg
$manifestFile = Resolve-Manifest $ManifestPath
$manifest = Get-Content -LiteralPath $manifestFile -Raw | ConvertFrom-Json
$tracks = @($manifest.tracks)
@@ -241,6 +343,7 @@ New-Item -ItemType Directory -Force -Path $workRoot | Out-Null
Write-Host "Manifest: $manifestFile"
Write-Host "Output: $libraryRoot"
Write-Host "ffmpeg: $ffmpeg"
Write-Host "ffprobe: $ffprobe"
Write-Host "Tracks: $($tracks.Count)"
$index = 0
@@ -260,7 +363,8 @@ foreach ($track in $tracks) {
New-Item -ItemType Directory -Force -Path $trackFolder | Out-Null
$safeTitle = ConvertTo-SafeName $title $id
$shortId = Get-ShortId $id
$prefix = "{0} [{1}]" -f $safeTitle, $shortId
$trackNo = Get-NextTrackNumber $trackFolder
$prefix = "{0:D2} {1}" -f $trackNo, $safeTitle
$lyrics = Get-DeepValue $track @("prompt", "lyrics", "lyric", "text")
$style = Get-DeepValue $track @("gpt_description_prompt", "style", "tags", "description")
@@ -276,9 +380,11 @@ foreach ($track in $tracks) {
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
$rawCoverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')"
$trackCoverFile = Join-Path $workRoot "$prefix.embed-cover.jpg"
$coverFile = Join-Path $trackFolder "cover.jpg"
$metadataFile = Join-Path $trackFolder "metadata.json"
$flacFile = Join-Path $trackFolder ("01 {0}.flac" -f $safeTitle)
$metadataFile = Join-Path $trackFolder ("metadata.{0}.json" -f $shortId)
$lyricsFile = Join-Path $trackFolder ("{0}.lrc" -f $prefix)
$flacFile = Join-Path $trackFolder ("{0}.flac" -f $prefix)
Write-Host ""
Write-Host "[$index/$($tracks.Count)] $title"
@@ -298,16 +404,22 @@ foreach ($track in $tracks) {
if ($imageUrl) {
$coverDownloaded = Invoke-Download $imageUrl $rawCoverFile
if ($coverDownloaded) {
& $ffmpeg -hide_banner -y -i $rawCoverFile -frames:v 1 $coverFile
$coverOk = ($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $coverFile)
& $ffmpeg -hide_banner -y -i $rawCoverFile -frames:v 1 $trackCoverFile
$coverOk = ($LASTEXITCODE -eq 0) -and (Test-Path -LiteralPath $trackCoverFile)
if (-not $coverOk) {
Copy-Item -LiteralPath $rawCoverFile -Destination $coverFile -Force
$coverOk = Test-Path -LiteralPath $coverFile
Copy-Item -LiteralPath $rawCoverFile -Destination $trackCoverFile -Force
$coverOk = Test-Path -LiteralPath $trackCoverFile
}
if ($coverOk -and -not (Test-Path -LiteralPath $coverFile)) {
Copy-Item -LiteralPath $trackCoverFile -Destination $coverFile -Force
}
}
}
$track | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $metadataFile -Encoding UTF8
if ($lyrics) {
$lyrics | Set-Content -LiteralPath $lyricsFile -Encoding UTF8
}
$argsList = [System.Collections.Generic.List[string]]::new()
$argsList.Add("-hide_banner")
@@ -316,7 +428,7 @@ foreach ($track in $tracks) {
$argsList.Add($inputAudio)
if ($coverOk) {
$argsList.Add("-i")
$argsList.Add($coverFile)
$argsList.Add($trackCoverFile)
$argsList.Add("-map")
$argsList.Add("0:a")
$argsList.Add("-map")
@@ -332,20 +444,21 @@ foreach ($track in $tracks) {
$argsList.Add("flac")
$argsList.Add("-compression_level")
$argsList.Add("8")
Add-MetadataArg $argsList "title" $title
Add-MetadataArg $argsList "artist" $ArtistName
Add-MetadataArg $argsList "album" $albumTitle
Add-MetadataArg $argsList "TITLE" $title
Add-MetadataArg $argsList "ARTIST" $ArtistName
Add-MetadataArg $argsList "ALBUM" $albumTitle
Add-MetadataArg $argsList "album_artist" $ArtistName
Add-MetadataArg $argsList "ARTISTS" $ArtistName
Add-MetadataArg $argsList "track" "1/1"
Add-MetadataArg $argsList "track" "$trackNo"
Add-MetadataArg $argsList "disc" "1/1"
Add-MetadataArg $argsList "genre" $style
Add-MetadataArg $argsList "GENRE" $style
Add-MetadataArg $argsList "lyrics-XXX" $lyrics
Add-MetadataArg $argsList "description" $style
Add-MetadataArg $argsList "comment" "Suno clip id: $id"
Add-MetadataArg $argsList "date" $createdAt
Add-MetadataArg $argsList "encoder" "Codex SUNO helper"
Add-MetadataArg $argsList "suno_model" $model
Add-MetadataArg $argsList "DESCRIPTION" $style
Add-MetadataArg $argsList "COMMENT" "Suno clip id: $id"
Add-MetadataArg $argsList "DATE" $createdAt
Add-MetadataArg $argsList "ENCODER" "Codex SUNO helper"
Add-MetadataArg $argsList "SUNO_MODEL" $model
Add-MetadataArg $argsList "SUNO_CLIP_ID" $id
Add-MetadataArg $argsList "TMED" "Digital Media"
$argsList.Add($flacFile)
@@ -356,6 +469,7 @@ foreach ($track in $tracks) {
}
Write-Host "완료: $flacFile"
Write-AlbumNfo $trackFolder $albumTitle $ArtistName $style $ffprobe
}
if (-not $KeepWorkFiles -and (Test-Path -LiteralPath $workRoot)) {