Simplify Suno download controls to WAV workflow

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-01 22:55:36 +09:00
parent 2e3698b3c6
commit 3a195c8894
3 changed files with 15 additions and 69 deletions
+6 -6
View File
@@ -35,7 +35,7 @@ SUNO 화면 구조가 바뀌어 자동 입력이 실패하면, 패널의 `Title`
SUNO 곡 상세/플레이어 화면에서 `WAV + 커버 + 메타 다운로드`를 누르면 현재 페이지에서 감지된 곡 정보를 기준으로 아래 파일을 다운로드합니다. SUNO 곡 상세/플레이어 화면에서 `WAV + 커버 + 메타 다운로드`를 누르면 현재 페이지에서 감지된 곡 정보를 기준으로 아래 파일을 다운로드합니다.
SUNO 라이브러리 목록 화면에서는 `라이브러리 곡 감지`를 누른 뒤 `처리용 manifest 저장`을 누릅니다. 현재 브라우저에 로드된 곡 목록을 기준으로 처리하므로, 무한 스크롤 목록에서는 먼저 아래쪽까지 스크롤해서 곡을 로드해야 합니다. SUNO 라이브러리 목록 화면에서는 `곡 감지`를 누른 뒤 `WAV + 메타데이터 다운`을 누릅니다. 현재 브라우저에 로드된 곡 목록을 기준으로 처리하므로, 무한 스크롤 목록에서는 먼저 아래쪽까지 스크롤해서 곡을 로드해야 합니다.
- `{title}.wav`: clip id가 감지되면 `https://cdn1.suno.ai/{clipId}.wav` 후보를 내려받습니다. - `{title}.wav`: clip id가 감지되면 `https://cdn1.suno.ai/{clipId}.wav` 후보를 내려받습니다.
- `{title}.cover.*`: 페이지에서 감지된 앨범아트 URL을 내려받습니다. - `{title}.cover.*`: 페이지에서 감지된 앨범아트 URL을 내려받습니다.
@@ -48,15 +48,15 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책
브라우저는 `\\VaultOfData\AKAMedia\음악\처리됨\SUNO` 같은 로컬/NAS 경로에 직접 파일을 정리하거나 WAV를 FLAC으로 인코딩할 수 없습니다. 그래서 Tampermonkey는 manifest를 만들고, 로컬 PowerShell 스크립트가 실제 다운로드/변환/정리를 담당합니다. 브라우저는 `\\VaultOfData\AKAMedia\음악\처리됨\SUNO` 같은 로컬/NAS 경로에 직접 파일을 정리하거나 WAV를 FLAC으로 인코딩할 수 없습니다. 그래서 Tampermonkey는 manifest를 만들고, 로컬 PowerShell 스크립트가 실제 다운로드/변환/정리를 담당합니다.
1. SUNO 라이브러리에서 `라이브러리 곡 감지`를 누릅니다. 1. SUNO 라이브러리에서 `곡 감지`를 누릅니다.
2. `처리용 manifest 저장`을 눌러 Downloads 폴더에 `suno-library-YYYY-MM-DD.manifest.json`저장합니다. 이 단계에서 브라우저가 SUNO의 `convert_wav/``wav_file/` API를 호출해 WAV URL을 준비합니다. 2. `WAV + 메타데이터 다운`누릅니다. 이 단계에서 브라우저가 SUNO의 `convert_wav/``wav_file/` API를 호출해 WAV URL을 준비합니다.
3. `run_process_suno_library.ps1`을 실행합니다. 3. `run_process_suno_library.ps1`을 실행합니다.
더 편한 흐름: 더 편한 흐름:
1. 먼저 `run_suno_companion.ps1`을 실행해서 로컬 수신 프로그램을 켜둡니다. 1. 먼저 `run_suno_companion.ps1`을 실행해서 로컬 수신 프로그램을 켜둡니다.
2. SUNO 라이브러리에서 `라이브러리 곡 감지`를 누릅니다. 2. SUNO 라이브러리에서 `곡 감지`를 누릅니다.
3. `로컬 프로그램으로 처리` 누릅니다. 3. `WAV + 메타데이터 다운` 누릅니다.
이 경우 Tampermonkey가 manifest를 `http://127.0.0.1:17873/process`로 보내고, 로컬 프로그램이 자동으로 다운로드/FLAC 변환/정리를 시작합니다. 이 경우 Tampermonkey가 manifest를 `http://127.0.0.1:17873/process`로 보내고, 로컬 프로그램이 자동으로 다운로드/FLAC 변환/정리를 시작합니다.
@@ -66,6 +66,6 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\YYYY-MM-DD \\VaultOfData\AKAMedia\음악\처리됨\SUNO\YYYY-MM-DD
``` ```
로컬 처리기는 manifest에 저장된 WAV URL 다운로드하고, 실패하면 manifest의 audio URL로 fallback합니다. 이후 ffmpeg로 FLAC 변환하면서 title, artist, album, genre/style, lyrics, Suno clip id, cover art를 메타데이터로 넣습니다. 로컬 처리기는 manifest에 저장된 WAV URL 다운로드합니다. MP3/audio fallback은 사용하지 않습니다. 이후 ffmpeg로 FLAC 변환하면서 title, artist, album, genre/style, lyrics, Suno clip id, cover art를 메타데이터로 넣습니다.
로그는 `companion-logs` 폴더에 저장됩니다. 로그는 `companion-logs` 폴더에 저장됩니다.
+8 -54
View File
@@ -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.0 // @version 0.5.1
// @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/*
@@ -895,16 +895,8 @@
if (preparedWavUrl) { if (preparedWavUrl) {
downloadUrl(preparedWavUrl, `${baseName}.wav`, (reason) => { downloadUrl(preparedWavUrl, `${baseName}.wav`, (reason) => {
fail("wav")(reason); fail("wav")(reason);
if (track.audioUrl && track.audioUrl !== preparedWavUrl) {
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio-fallback"));
messages.push("audio fallback 요청");
setStatus(statusTarget, messages.join(" / "));
}
}); });
messages.push("wav 요청"); messages.push("wav 요청");
} else if (track.audioUrl) {
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio"));
messages.push("audio 요청");
} }
setStatus(statusTarget, messages.join(" / ")); setStatus(statusTarget, messages.join(" / "));
} }
@@ -935,7 +927,7 @@
async function exportSunoManifest(tracks, statusTarget) { async function exportSunoManifest(tracks, statusTarget) {
if (!tracks.length) { if (!tracks.length) {
setStatus(statusTarget, "내보낼 곡을 못 찾았어. 라이브러리 곡 감지를 먼저 해줘."); setStatus(statusTarget, "내보낼 곡을 못 찾았어. 곡 감지를 먼저 해줘.");
return; return;
} }
const preparedTracks = []; const preparedTracks = [];
@@ -957,7 +949,7 @@
async function postToCompanion(tracks, statusTarget) { async function postToCompanion(tracks, statusTarget) {
if (!tracks.length) { if (!tracks.length) {
setStatus(statusTarget, "보낼 곡을 못 찾았어. 라이브러리 곡 감지를 먼저 해줘."); setStatus(statusTarget, "보낼 곡을 못 찾았어. 곡 감지를 먼저 해줘.");
return; return;
} }
const preparedTracks = []; const preparedTracks = [];
@@ -1001,22 +993,10 @@
${FIELD_DEFS.map((def) => `<button class="cgs-btn" type="button" data-cgs-copy="${def.key}">${escapeHtml(def.label)}</button>`).join("")} ${FIELD_DEFS.map((def) => `<button class="cgs-btn" type="button" data-cgs-copy="${def.key}">${escapeHtml(def.label)}</button>`).join("")}
</div> </div>
<div class="cgs-row"> <div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-refresh-track>곡 감지</button> <button class="cgs-btn cgs-wide" type="button" data-cgs-scan-library>곡 감지</button>
</div> </div>
<div class="cgs-row"> <div class="cgs-row">
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-download-track>WAV + 커버 + 메타 다운로드</button> <button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-library>WAV + 메타데이터 다운</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-scan-library>라이브러리 곡 감지</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-export-library>처리용 manifest 저장</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-send-companion>로컬 프로그램으로 처리</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-download-library>브라우저 일괄 다운로드</button>
</div> </div>
<div class="cgs-status"></div> <div class="cgs-status"></div>
`; `;
@@ -1059,21 +1039,6 @@
}); });
} }
body.querySelector("[data-cgs-refresh-track]").addEventListener("click", () => {
updateSummary();
const track = findCurrentSunoTrack();
setStatus(body, track.id || track.audioUrl ? `감지됨: ${track.title || track.id}` : "현재 화면에서 곡 URL을 못 찾았어.");
});
body.querySelector("[data-cgs-download-track]").addEventListener("click", async () => {
const track = findCurrentSunoTrack();
if (!track.id && !track.audioUrl && !track.imageUrl) {
setStatus(body, "다운로드할 곡 정보를 못 찾았어. 곡 상세/플레이어 화면에서 다시 눌러봐.");
return;
}
await downloadSunoPackage(track, body);
});
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => { body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
libraryTracks = scanSunoLibraryTracks(); libraryTracks = scanSunoLibraryTracks();
updateSummary(); updateSummary();
@@ -1088,25 +1053,14 @@
setStatus(body, `${networkHint} 목록 새로고침이나 스크롤 후 다시 눌러봐. 화면 텍스트: ${visibleHints.join(", ")}`); setStatus(body, `${networkHint} 목록 새로고침이나 스크롤 후 다시 눌러봐. 화면 텍스트: ${visibleHints.join(", ")}`);
}); });
body.querySelector("[data-cgs-download-library]").addEventListener("click", async () => { body.querySelector("[data-cgs-process-library]").addEventListener("click", async () => {
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
updateSummary();
await downloadSunoBatch(libraryTracks, body);
});
body.querySelector("[data-cgs-export-library]").addEventListener("click", async () => {
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
updateSummary();
await exportSunoManifest(libraryTracks, body);
});
body.querySelector("[data-cgs-send-companion]").addEventListener("click", async () => {
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks(); if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
updateSummary(); updateSummary();
try { try {
await postToCompanion(libraryTracks, body); await postToCompanion(libraryTracks, body);
} catch (error) { } catch (error) {
setStatus(body, `로컬 프로그램 연결 실패: ${error.message || error}`); setStatus(body, `로컬 프로그램 연결 실패. manifest로 저장할게: ${error.message || error}`);
await exportSunoManifest(libraryTracks, body);
} }
}); });
} }
+1 -9
View File
@@ -191,7 +191,6 @@ foreach ($track in $tracks) {
$model = Get-DeepValue $track @("model_name", "model", "major_model_version") $model = Get-DeepValue $track @("model_name", "model", "major_model_version")
$createdAt = Get-DeepValue $track @("created_at", "createdAt") $createdAt = Get-DeepValue $track @("created_at", "createdAt")
$audioUrl = Resolve-SunoMediaUrl $(if ($track.audioUrl) { "$($track.audioUrl)" } else { Get-DeepValue $track @("audio_url", "audioUrl", "stream_audio_url", "streamAudioUrl", "play_url", "playUrl", "play_path", "playPath", "mp3_url", "mp3Url", "video_url", "videoUrl") })
$wavUrl = Resolve-SunoMediaUrl $(if ($track.wavUrl) { "$($track.wavUrl)" } else { Get-DeepValue $track @("wav_url", "wavUrl", "download_url", "downloadUrl") }) $wavUrl = Resolve-SunoMediaUrl $(if ($track.wavUrl) { "$($track.wavUrl)" } else { Get-DeepValue $track @("wav_url", "wavUrl", "download_url", "downloadUrl") })
$wavCandidates = @() $wavCandidates = @()
if ($wavUrl) { $wavCandidates += $wavUrl } if ($wavUrl) { $wavCandidates += $wavUrl }
@@ -200,7 +199,6 @@ 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"
$fallbackAudio = Join-Path $workRoot "$prefix.input.$(Get-UrlExtension $audioUrl 'audio')"
$coverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')" $coverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')"
$metadataFile = Join-Path $targetRoot "$prefix.metadata.json" $metadataFile = Join-Path $targetRoot "$prefix.metadata.json"
$flacFile = Join-Path $targetRoot "$prefix.flac" $flacFile = Join-Path $targetRoot "$prefix.flac"
@@ -214,14 +212,8 @@ foreach ($track in $tracks) {
$audioOk = Invoke-Download $candidate $inputAudio $audioOk = Invoke-Download $candidate $inputAudio
if ($audioOk) { break } if ($audioOk) { break }
} }
if (-not $audioOk -and $audioUrl) {
Write-Host "WAV 후보 실패. audioUrl로 fallback 시도."
$audioOk = Invoke-Download $audioUrl $fallbackAudio
if ($audioOk) { $inputAudio = $fallbackAudio }
}
if (-not $audioOk) { if (-not $audioOk) {
Write-Warning "오디오를 받지 못해서 건너뜀: $title" Write-Warning "WAV를 받지 못해서 건너뜀: $title"
continue continue
} }