Add clip-id based Suno library sync
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
companion-inbox/
|
||||||
|
companion-logs/
|
||||||
|
_work/
|
||||||
@@ -63,9 +63,19 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책
|
|||||||
기본 출력 위치:
|
기본 출력 위치:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\YYYY-MM-DD
|
\\VaultOfData\AKAMedia\음악\처리됨\SUNO
|
||||||
```
|
```
|
||||||
|
|
||||||
로컬 처리기는 manifest에 저장된 WAV URL만 다운로드합니다. MP3/audio fallback은 사용하지 않습니다. 이후 ffmpeg로 FLAC 변환하면서 title, artist, album, genre/style, lyrics, Suno clip id, cover art를 메타데이터로 넣습니다.
|
정리 포맷:
|
||||||
|
|
||||||
|
```text
|
||||||
|
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\곡 제목 [clipId앞12자리]\01 곡 제목.flac
|
||||||
|
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\곡 제목 [clipId앞12자리]\cover.jpg
|
||||||
|
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\곡 제목 [clipId앞12자리]\metadata.json
|
||||||
|
```
|
||||||
|
|
||||||
|
동기화 비교 키는 곡 제목이 아니라 SUNO clip id입니다. 같은 제목의 곡이 여러 개 있어도 `metadata.json`의 `id`와 폴더명의 id 조각으로 구분합니다.
|
||||||
|
|
||||||
|
로컬 처리기는 manifest에 저장된 WAV URL만 다운로드합니다. MP3/audio fallback은 사용하지 않습니다. 이후 ffmpeg로 FLAC 변환하면서 기존 라이브러리 방식에 맞춰 title, artist, album, album_artist, track, disc, genre/style, lyrics-XXX, Suno clip id, cover art를 메타데이터로 넣습니다.
|
||||||
|
|
||||||
로그는 `companion-logs` 폴더에 저장됩니다.
|
로그는 `companion-logs` 폴더에 저장됩니다.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name ChatGPT to Suno Prompt Copier
|
// @name ChatGPT to Suno Prompt Copier
|
||||||
// @namespace local.suno-helper
|
// @namespace local.suno-helper
|
||||||
// @version 0.5.2
|
// @version 0.6.0
|
||||||
// @description Copy structured ChatGPT song prompt versions into Suno fields.
|
// @description Copy structured ChatGPT song prompt versions into Suno fields.
|
||||||
// @author local
|
// @author local
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
@@ -952,10 +952,33 @@
|
|||||||
setStatus(statusTarget, "보낼 곡을 못 찾았어. 곡 감지를 먼저 해줘.");
|
setStatus(statusTarget, "보낼 곡을 못 찾았어. 곡 감지를 먼저 해줘.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const syncManifest = {
|
||||||
|
pageUrl: location.href,
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
count: tracks.length,
|
||||||
|
tracks,
|
||||||
|
};
|
||||||
|
setStatus(statusTarget, `${tracks.length}곡 로컬 폴더와 비교 중...`);
|
||||||
|
const syncResponse = await fetch("http://127.0.0.1:17873/sync-index", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify(syncManifest),
|
||||||
|
});
|
||||||
|
const syncText = await syncResponse.text();
|
||||||
|
if (!syncResponse.ok) {
|
||||||
|
throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`);
|
||||||
|
}
|
||||||
|
const syncData = tryParseJson(syncText) || {};
|
||||||
|
const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : [];
|
||||||
|
if (!missingTracks.length) {
|
||||||
|
setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const preparedTracks = [];
|
const preparedTracks = [];
|
||||||
for (let index = 0; index < tracks.length; index += 1) {
|
for (let index = 0; index < missingTracks.length; index += 1) {
|
||||||
const track = { ...tracks[index] };
|
const track = { ...missingTracks[index] };
|
||||||
setStatus(statusTarget, `${index + 1}/${tracks.length} WAV 준비 중: ${track.title || track.id}`);
|
setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 준비 중: ${track.title || track.id}`);
|
||||||
track.wavUrl = await requestSunoWavUrl(track);
|
track.wavUrl = await requestSunoWavUrl(track);
|
||||||
preparedTracks.push(track);
|
preparedTracks.push(track);
|
||||||
}
|
}
|
||||||
@@ -975,7 +998,11 @@
|
|||||||
throw new Error(text || `${response.status} ${response.statusText}`);
|
throw new Error(text || `${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
const data = tryParseJson(text) || {};
|
const data = tryParseJson(text) || {};
|
||||||
setStatus(statusTarget, `로컬 처리 시작: ${data.jobId || "job"} / ${data.log || "companion console 확인"}`);
|
if (data.skipped) {
|
||||||
|
setStatus(statusTarget, "동기화 완료 상태: 처리할 새 곡 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus(statusTarget, `로컬 처리 시작: ${data.jobId || "job"} / 새 곡 ${preparedTracks.length}개`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testCompanion(statusTarget) {
|
async function testCompanion(statusTarget) {
|
||||||
|
|||||||
+108
-16
@@ -1,8 +1,10 @@
|
|||||||
param(
|
param(
|
||||||
[string]$ManifestPath = "",
|
[string]$ManifestPath = "",
|
||||||
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨\SUNO",
|
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨",
|
||||||
|
[string]$ArtistName = "SUNO",
|
||||||
[string]$FfmpegPath = "",
|
[string]$FfmpegPath = "",
|
||||||
[switch]$KeepWorkFiles
|
[switch]$KeepWorkFiles,
|
||||||
|
[switch]$Force
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -64,6 +66,70 @@ function ConvertTo-SafeName {
|
|||||||
return $name
|
return $name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-ShortId {
|
||||||
|
param([string]$Id)
|
||||||
|
|
||||||
|
if (-not $Id) { return "unknown-id" }
|
||||||
|
if ($Id.Length -le 12) { return $Id }
|
||||||
|
return $Id.Substring(0, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-TrackFolder {
|
||||||
|
param(
|
||||||
|
[string]$Root,
|
||||||
|
[string]$Title,
|
||||||
|
[string]$Id
|
||||||
|
)
|
||||||
|
|
||||||
|
$safeTitle = ConvertTo-SafeName $Title $Id
|
||||||
|
$shortId = Get-ShortId $Id
|
||||||
|
return Join-Path $Root ("{0} [{1}]" -f $safeTitle, $shortId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AlbumTitle {
|
||||||
|
param(
|
||||||
|
[string]$Title,
|
||||||
|
[string]$Id
|
||||||
|
)
|
||||||
|
|
||||||
|
return "{0} [{1}]" -f (ConvertTo-SafeName $Title $Id), (Get-ShortId $Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Resolve-LibraryRoot {
|
||||||
|
param(
|
||||||
|
[string]$Root,
|
||||||
|
[string]$Artist
|
||||||
|
)
|
||||||
|
|
||||||
|
$trimmed = $Root.TrimEnd("\", "/")
|
||||||
|
if ((Split-Path -Leaf $trimmed) -ieq $Artist) {
|
||||||
|
return $trimmed
|
||||||
|
}
|
||||||
|
return Join-Path $trimmed $Artist
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ExistingTrack {
|
||||||
|
param(
|
||||||
|
[string]$Root,
|
||||||
|
[string]$Id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $Id -or -not (Test-Path -LiteralPath $Root)) { return $false }
|
||||||
|
|
||||||
|
$metadataHit = Get-ChildItem -LiteralPath $Root -Recurse -Filter "*metadata.json" -File -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object {
|
||||||
|
try {
|
||||||
|
$json = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
|
||||||
|
"$($json.id)" -eq $Id
|
||||||
|
} catch {
|
||||||
|
$false
|
||||||
|
}
|
||||||
|
} |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
return [bool]$metadataHit
|
||||||
|
}
|
||||||
|
|
||||||
function Get-ObjectValues {
|
function Get-ObjectValues {
|
||||||
param($Value)
|
param($Value)
|
||||||
|
|
||||||
@@ -167,13 +233,13 @@ if (-not $tracks.Count) {
|
|||||||
throw "manifest에 tracks 배열이 없어."
|
throw "manifest에 tracks 배열이 없어."
|
||||||
}
|
}
|
||||||
|
|
||||||
$dateFolder = Get-Date -Format "yyyy-MM-dd"
|
$libraryRoot = Resolve-LibraryRoot $OutputRoot $ArtistName
|
||||||
$targetRoot = Join-Path $OutputRoot $dateFolder
|
New-Item -ItemType Directory -Force -Path $libraryRoot | Out-Null
|
||||||
$workRoot = Join-Path $targetRoot "_work"
|
$workRoot = Join-Path $libraryRoot "_work"
|
||||||
New-Item -ItemType Directory -Force -Path $targetRoot, $workRoot | Out-Null
|
New-Item -ItemType Directory -Force -Path $workRoot | Out-Null
|
||||||
|
|
||||||
Write-Host "Manifest: $manifestFile"
|
Write-Host "Manifest: $manifestFile"
|
||||||
Write-Host "Output: $targetRoot"
|
Write-Host "Output: $libraryRoot"
|
||||||
Write-Host "ffmpeg: $ffmpeg"
|
Write-Host "ffmpeg: $ffmpeg"
|
||||||
Write-Host "Tracks: $($tracks.Count)"
|
Write-Host "Tracks: $($tracks.Count)"
|
||||||
|
|
||||||
@@ -183,8 +249,18 @@ foreach ($track in $tracks) {
|
|||||||
|
|
||||||
$id = if ($track.id) { "$($track.id)" } else { "" }
|
$id = if ($track.id) { "$($track.id)" } else { "" }
|
||||||
$title = if ($track.title) { "$($track.title)" } else { $id }
|
$title = if ($track.title) { "$($track.title)" } else { $id }
|
||||||
|
if (-not $Force -and (Test-ExistingTrack $libraryRoot $id)) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "[$index/$($tracks.Count)] 이미 있음, 건너뜀: $title [$id]"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$albumTitle = Get-AlbumTitle $title $id
|
||||||
|
$trackFolder = Get-TrackFolder $libraryRoot $title $id
|
||||||
|
New-Item -ItemType Directory -Force -Path $trackFolder | Out-Null
|
||||||
$safeTitle = ConvertTo-SafeName $title $id
|
$safeTitle = ConvertTo-SafeName $title $id
|
||||||
$prefix = "{0:D3} - {1}" -f $index, $safeTitle
|
$shortId = Get-ShortId $id
|
||||||
|
$prefix = "{0} [{1}]" -f $safeTitle, $shortId
|
||||||
|
|
||||||
$lyrics = Get-DeepValue $track @("prompt", "lyrics", "lyric", "text")
|
$lyrics = Get-DeepValue $track @("prompt", "lyrics", "lyric", "text")
|
||||||
$style = Get-DeepValue $track @("gpt_description_prompt", "style", "tags", "description")
|
$style = Get-DeepValue $track @("gpt_description_prompt", "style", "tags", "description")
|
||||||
@@ -199,9 +275,10 @@ foreach ($track in $tracks) {
|
|||||||
$imageUrl = if ($track.imageUrl) { "$($track.imageUrl)" } else { Get-DeepValue $track @("image_url", "imageUrl", "image_large_url", "cover_url") }
|
$imageUrl = if ($track.imageUrl) { "$($track.imageUrl)" } else { Get-DeepValue $track @("image_url", "imageUrl", "image_large_url", "cover_url") }
|
||||||
|
|
||||||
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
|
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
|
||||||
$coverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')"
|
$rawCoverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')"
|
||||||
$metadataFile = Join-Path $targetRoot "$prefix.metadata.json"
|
$coverFile = Join-Path $trackFolder "cover.jpg"
|
||||||
$flacFile = Join-Path $targetRoot "$prefix.flac"
|
$metadataFile = Join-Path $trackFolder "metadata.json"
|
||||||
|
$flacFile = Join-Path $trackFolder ("01 {0}.flac" -f $safeTitle)
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "[$index/$($tracks.Count)] $title"
|
Write-Host "[$index/$($tracks.Count)] $title"
|
||||||
@@ -219,7 +296,15 @@ foreach ($track in $tracks) {
|
|||||||
|
|
||||||
$coverOk = $false
|
$coverOk = $false
|
||||||
if ($imageUrl) {
|
if ($imageUrl) {
|
||||||
$coverOk = Invoke-Download $imageUrl $coverFile
|
$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)
|
||||||
|
if (-not $coverOk) {
|
||||||
|
Copy-Item -LiteralPath $rawCoverFile -Destination $coverFile -Force
|
||||||
|
$coverOk = Test-Path -LiteralPath $coverFile
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$track | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $metadataFile -Encoding UTF8
|
$track | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $metadataFile -Encoding UTF8
|
||||||
@@ -240,21 +325,28 @@ foreach ($track in $tracks) {
|
|||||||
$argsList.Add("attached_pic")
|
$argsList.Add("attached_pic")
|
||||||
$argsList.Add("-c:v")
|
$argsList.Add("-c:v")
|
||||||
$argsList.Add("copy")
|
$argsList.Add("copy")
|
||||||
|
$argsList.Add("-metadata:s:v")
|
||||||
|
$argsList.Add("comment=Cover (front)")
|
||||||
}
|
}
|
||||||
$argsList.Add("-c:a")
|
$argsList.Add("-c:a")
|
||||||
$argsList.Add("flac")
|
$argsList.Add("flac")
|
||||||
$argsList.Add("-compression_level")
|
$argsList.Add("-compression_level")
|
||||||
$argsList.Add("8")
|
$argsList.Add("8")
|
||||||
Add-MetadataArg $argsList "title" $title
|
Add-MetadataArg $argsList "title" $title
|
||||||
Add-MetadataArg $argsList "artist" "SUNO"
|
Add-MetadataArg $argsList "artist" $ArtistName
|
||||||
Add-MetadataArg $argsList "album" "SUNO"
|
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 "disc" "1/1"
|
||||||
Add-MetadataArg $argsList "genre" $style
|
Add-MetadataArg $argsList "genre" $style
|
||||||
Add-MetadataArg $argsList "lyrics" $lyrics
|
Add-MetadataArg $argsList "lyrics-XXX" $lyrics
|
||||||
Add-MetadataArg $argsList "description" $style
|
Add-MetadataArg $argsList "description" $style
|
||||||
Add-MetadataArg $argsList "comment" "Suno clip id: $id"
|
Add-MetadataArg $argsList "comment" "Suno clip id: $id"
|
||||||
Add-MetadataArg $argsList "date" $createdAt
|
Add-MetadataArg $argsList "date" $createdAt
|
||||||
Add-MetadataArg $argsList "encoder" "Codex SUNO helper"
|
Add-MetadataArg $argsList "encoder" "Codex SUNO helper"
|
||||||
Add-MetadataArg $argsList "suno_model" $model
|
Add-MetadataArg $argsList "suno_model" $model
|
||||||
|
Add-MetadataArg $argsList "TMED" "Digital Media"
|
||||||
$argsList.Add($flacFile)
|
$argsList.Add($flacFile)
|
||||||
|
|
||||||
& $ffmpeg @argsList
|
& $ffmpeg @argsList
|
||||||
@@ -271,4 +363,4 @@ if (-not $KeepWorkFiles -and (Test-Path -LiteralPath $workRoot)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "처리 완료: $targetRoot"
|
Write-Host "처리 완료: $libraryRoot"
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ if (-not $latest) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
& "$PSScriptRoot\process-suno-library.ps1" -ManifestPath $latest.FullName -OutputRoot "\\VaultOfData\AKAMedia\음악\처리됨\SUNO"
|
& "$PSScriptRoot\process-suno-library.ps1" `
|
||||||
|
-ManifestPath $latest.FullName `
|
||||||
|
-OutputRoot "\\VaultOfData\AKAMedia\음악\처리됨" `
|
||||||
|
-ArtistName "SUNO"
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Read-Host "Enter를 누르면 닫힘"
|
Read-Host "Enter를 누르면 닫힘"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ $ErrorActionPreference = "Stop"
|
|||||||
|
|
||||||
& "$PSScriptRoot\suno-companion.ps1" `
|
& "$PSScriptRoot\suno-companion.ps1" `
|
||||||
-Port 17873 `
|
-Port 17873 `
|
||||||
-OutputRoot "\\VaultOfData\AKAMedia\음악\처리됨\SUNO"
|
-OutputRoot "\\VaultOfData\AKAMedia\음악\처리됨" `
|
||||||
|
-ArtistName "SUNO"
|
||||||
|
|
||||||
Read-Host "Enter를 누르면 닫힘"
|
Read-Host "Enter를 누르면 닫힘"
|
||||||
|
|||||||
+93
-5
@@ -1,6 +1,7 @@
|
|||||||
param(
|
param(
|
||||||
[int]$Port = 17873,
|
[int]$Port = 17873,
|
||||||
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨\SUNO",
|
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨",
|
||||||
|
[string]$ArtistName = "SUNO",
|
||||||
[string]$FfmpegPath = ""
|
[string]$FfmpegPath = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,6 +13,21 @@ $inbox = Join-Path $scriptRoot "companion-inbox"
|
|||||||
$logs = Join-Path $scriptRoot "companion-logs"
|
$logs = Join-Path $scriptRoot "companion-logs"
|
||||||
New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null
|
New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null
|
||||||
|
|
||||||
|
function Resolve-LibraryRoot {
|
||||||
|
param(
|
||||||
|
[string]$Root,
|
||||||
|
[string]$Artist
|
||||||
|
)
|
||||||
|
|
||||||
|
$trimmed = $Root.TrimEnd("\", "/")
|
||||||
|
if ((Split-Path -Leaf $trimmed) -ieq $Artist) {
|
||||||
|
return $trimmed
|
||||||
|
}
|
||||||
|
return Join-Path $trimmed $Artist
|
||||||
|
}
|
||||||
|
|
||||||
|
$libraryRoot = Resolve-LibraryRoot $OutputRoot $ArtistName
|
||||||
|
|
||||||
function Write-Response {
|
function Write-Response {
|
||||||
param(
|
param(
|
||||||
[System.Net.Sockets.NetworkStream]$Stream,
|
[System.Net.Sockets.NetworkStream]$Stream,
|
||||||
@@ -103,13 +119,16 @@ function Start-SunoProcess {
|
|||||||
"-ExecutionPolicy", "Bypass",
|
"-ExecutionPolicy", "Bypass",
|
||||||
"-File", $processor,
|
"-File", $processor,
|
||||||
"-ManifestPath", $ManifestPath,
|
"-ManifestPath", $ManifestPath,
|
||||||
"-OutputRoot", $OutputRoot
|
"-OutputRoot", $OutputRoot,
|
||||||
|
"-ArtistName", $ArtistName
|
||||||
)
|
)
|
||||||
if ($FfmpegPath) {
|
if ($FfmpegPath) {
|
||||||
$args += @("-FfmpegPath", $FfmpegPath)
|
$args += @("-FfmpegPath", $FfmpegPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
$process = Start-Process -FilePath "powershell.exe" -ArgumentList $args -PassThru -NoNewWindow -RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
$shellPath = (Get-Process -Id $PID).Path
|
||||||
|
if (-not $shellPath) { $shellPath = "pwsh.exe" }
|
||||||
|
$process = Start-Process -FilePath $shellPath -ArgumentList $args -PassThru -NoNewWindow -RedirectStandardOutput $stdout -RedirectStandardError $stderr
|
||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
jobId = $jobId
|
jobId = $jobId
|
||||||
pid = $process.Id
|
pid = $process.Id
|
||||||
@@ -118,6 +137,44 @@ function Start-SunoProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-ExistingSunoIds {
|
||||||
|
param([string]$Root)
|
||||||
|
|
||||||
|
$ids = [System.Collections.Generic.HashSet[string]]::new()
|
||||||
|
if (-not (Test-Path -LiteralPath $Root)) {
|
||||||
|
return ,$ids
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-ChildItem -LiteralPath $Root -Recurse -Filter "*metadata.json" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
try {
|
||||||
|
$json = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
|
||||||
|
$id = "$($json.id)".Trim()
|
||||||
|
if ($id) { [void]$ids.Add($id) }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ,$ids
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-MissingTracks {
|
||||||
|
param($Tracks)
|
||||||
|
|
||||||
|
$existingIds = Get-ExistingSunoIds $libraryRoot
|
||||||
|
$missing = @()
|
||||||
|
foreach ($track in @($Tracks)) {
|
||||||
|
$id = "$($track.id)".Trim()
|
||||||
|
if (-not $id) { continue }
|
||||||
|
if (-not $existingIds.Contains($id)) {
|
||||||
|
$missing += $track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
existingIds = @($existingIds)
|
||||||
|
missing = $missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $processor)) {
|
if (-not (Test-Path -LiteralPath $processor)) {
|
||||||
throw "processor not found: $processor"
|
throw "processor not found: $processor"
|
||||||
}
|
}
|
||||||
@@ -127,6 +184,7 @@ $listener.Start()
|
|||||||
|
|
||||||
Write-Host "SUNO companion listening on http://127.0.0.1:$Port"
|
Write-Host "SUNO companion listening on http://127.0.0.1:$Port"
|
||||||
Write-Host "OutputRoot: $OutputRoot"
|
Write-Host "OutputRoot: $OutputRoot"
|
||||||
|
Write-Host "Library: $libraryRoot"
|
||||||
Write-Host "Logs: $logs"
|
Write-Host "Logs: $logs"
|
||||||
Write-Host "Press Ctrl+C to stop."
|
Write-Host "Press Ctrl+C to stop."
|
||||||
|
|
||||||
@@ -144,7 +202,24 @@ try {
|
|||||||
|
|
||||||
if ($request.Method -eq "GET" -and $request.Path -eq "/health") {
|
if ($request.Method -eq "GET" -and $request.Path -eq "/health") {
|
||||||
Write-Host "Health check OK: $(Get-Date -Format 'HH:mm:ss')"
|
Write-Host "Health check OK: $(Get-Date -Format 'HH:mm:ss')"
|
||||||
Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot } | ConvertTo-Json -Compress)
|
Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot; libraryRoot = $libraryRoot; artist = $ArtistName } | ConvertTo-Json -Compress)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request.Method -eq "POST" -and $request.Path -eq "/sync-index") {
|
||||||
|
$manifest = $request.Body | ConvertFrom-Json
|
||||||
|
$result = Get-MissingTracks $manifest.tracks
|
||||||
|
Write-Host "Sync check: $(@($manifest.tracks).Count) received, $(@($result.missing).Count) missing"
|
||||||
|
Write-Response $stream 200 "OK" (@{
|
||||||
|
ok = $true
|
||||||
|
outputRoot = $OutputRoot
|
||||||
|
libraryRoot = $libraryRoot
|
||||||
|
artist = $ArtistName
|
||||||
|
received = @($manifest.tracks).Count
|
||||||
|
existing = @($result.existingIds).Count
|
||||||
|
missing = @($result.missing).Count
|
||||||
|
tracks = @($result.missing)
|
||||||
|
} | ConvertTo-Json -Depth 50 -Compress)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,8 +230,21 @@ try {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sync = Get-MissingTracks $manifest.tracks
|
||||||
|
$manifest.tracks = @($sync.missing)
|
||||||
|
$manifest.count = @($manifest.tracks).Count
|
||||||
|
if ($manifest.count -eq 0) {
|
||||||
|
Write-Host "Process skipped: library already synced"
|
||||||
|
Write-Response $stream 200 "OK" (@{
|
||||||
|
ok = $true
|
||||||
|
skipped = $true
|
||||||
|
message = "library already synced"
|
||||||
|
} | ConvertTo-Json -Compress)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
$manifestPath = Join-Path $inbox ("suno-library-{0}.manifest.json" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
|
$manifestPath = Join-Path $inbox ("suno-library-{0}.manifest.json" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
|
||||||
$request.Body | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
$manifest | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
||||||
$job = Start-SunoProcess $manifestPath
|
$job = Start-SunoProcess $manifestPath
|
||||||
|
|
||||||
Write-Host "Started job $($job.jobId) for $(@($manifest.tracks).Count) tracks"
|
Write-Host "Started job $($job.jobId) for $(@($manifest.tracks).Count) tracks"
|
||||||
|
|||||||
Reference in New Issue
Block a user