Compare commits
5 Commits
8fb7dbb250
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a77a40695c | |||
| d6a7e50ad3 | |||
| 46b9be652d | |||
| abcb2e69b7 | |||
| 33aa56e571 |
@@ -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.9.8
|
// @version 0.10.3
|
||||||
// @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/*
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const SCRIPT_VERSION = "0.9.8";
|
const SCRIPT_VERSION = "0.10.3";
|
||||||
const STORAGE_KEY = "chatgptSunoPromptSlots";
|
const STORAGE_KEY = "chatgptSunoPromptSlots";
|
||||||
const SLOT_COUNT = 2;
|
const SLOT_COUNT = 2;
|
||||||
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
|
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
|
||||||
@@ -75,15 +75,18 @@
|
|||||||
|
|
||||||
const originalFetch = page.fetch;
|
const originalFetch = page.fetch;
|
||||||
if (typeof originalFetch === "function") {
|
if (typeof originalFetch === "function") {
|
||||||
page.fetch = async function (...args) {
|
page.fetch = function (...args) {
|
||||||
const response = await originalFetch.apply(this, args);
|
const result = originalFetch.apply(this, args);
|
||||||
const url = args[0]?.url || args[0] || response.url || "";
|
result.then((response) => {
|
||||||
if (/suno/i.test(String(url))) {
|
const url = args[0]?.url || args[0] || response.url || "";
|
||||||
response.clone().text().then((text) => {
|
const urlText = String(url);
|
||||||
rememberSunoResponse(url, tryParseJson(text) || text);
|
if (/suno/i.test(urlText) && !/agg-receiver-service|datadoghq|braze|sprig|cloudflareinsights/i.test(urlText)) {
|
||||||
}).catch(() => {});
|
response.clone().text().then((text) => {
|
||||||
}
|
rememberSunoResponse(url, tryParseJson(text) || text);
|
||||||
return response;
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,7 +972,7 @@
|
|||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findTrackRowElement(track) {
|
function findTrackRowElementNow(track) {
|
||||||
const rows = getVisibleLibraryRows();
|
const rows = getVisibleLibraryRows();
|
||||||
const index = getTrackVisibleIndex(track, rows);
|
const index = getTrackVisibleIndex(track, rows);
|
||||||
if (index >= 0) return rows[index].element;
|
if (index >= 0) return rows[index].element;
|
||||||
@@ -978,6 +981,42 @@
|
|||||||
return rows.find((row) => row.normalized.includes(title))?.element || null;
|
return rows.find((row) => row.normalized.includes(title))?.element || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPageScroller() {
|
||||||
|
return document.scrollingElement || document.documentElement || document.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findTrackRowElement(track, statusTarget) {
|
||||||
|
const current = findTrackRowElementNow(track);
|
||||||
|
if (current) return current;
|
||||||
|
|
||||||
|
const scroller = getPageScroller();
|
||||||
|
const originalTop = scroller.scrollTop || window.scrollY || 0;
|
||||||
|
const maxSteps = 50;
|
||||||
|
const step = Math.max(260, Math.floor(window.innerHeight * 0.72));
|
||||||
|
|
||||||
|
setStatus(statusTarget, `SUNO 화면에서 곡 행 찾는 중: ${track.title || track.id}`);
|
||||||
|
scroller.scrollTop = 0;
|
||||||
|
window.scrollTo?.(0, 0);
|
||||||
|
await sleep(450);
|
||||||
|
|
||||||
|
for (let stepIndex = 0; stepIndex <= maxSteps; stepIndex += 1) {
|
||||||
|
const found = findTrackRowElementNow(track);
|
||||||
|
if (found) return found;
|
||||||
|
|
||||||
|
const before = scroller.scrollTop || window.scrollY || 0;
|
||||||
|
scroller.scrollTop = before + step;
|
||||||
|
window.scrollTo?.(0, before + step);
|
||||||
|
await sleep(260);
|
||||||
|
const after = scroller.scrollTop || window.scrollY || 0;
|
||||||
|
if (Math.abs(after - before) < 4) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
scroller.scrollTop = originalTop;
|
||||||
|
window.scrollTo?.(0, originalTop);
|
||||||
|
await sleep(200);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findMoreButtonInRow(row) {
|
function findMoreButtonInRow(row) {
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
const buttons = [...row.querySelectorAll("button, [role='button']")];
|
const buttons = [...row.querySelectorAll("button, [role='button']")];
|
||||||
@@ -1001,8 +1040,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function clickSunoUiDownload(track, format, statusTarget) {
|
async function clickSunoUiDownload(track, format, statusTarget) {
|
||||||
const row = findTrackRowElement(track);
|
const row = await findTrackRowElement(track, statusTarget);
|
||||||
if (!row) throw new Error("현재 화면에서 곡 행을 못 찾았어. SUNO 목록에서 그 곡이 보이게 스크롤한 뒤 다시 눌러줘.");
|
if (!row) throw new Error("SUNO 화면을 훑어도 곡 행을 못 찾았어. 필터/검색 때문에 목록에 안 보이는지 확인해줘.");
|
||||||
const moreButton = findMoreButtonInRow(row);
|
const moreButton = findMoreButtonInRow(row);
|
||||||
if (!moreButton) throw new Error("곡의 ... 메뉴 버튼을 못 찾았어.");
|
if (!moreButton) throw new Error("곡의 ... 메뉴 버튼을 못 찾았어.");
|
||||||
|
|
||||||
@@ -1721,6 +1760,30 @@
|
|||||||
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. ${syncTextSummary}. 로컬에서 메타데이터 후처리 중${healthText}`);
|
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. ${syncTextSummary}. 로컬에서 메타데이터 후처리 중${healthText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function expectManualDownloads(tracks, statusTarget, format = "mp3") {
|
||||||
|
const selected = tracks.filter((track) => track?.id);
|
||||||
|
if (!selected.length) {
|
||||||
|
setStatus(statusTarget, "등록할 곡이 없어. 곡 감지 후 목록에서 선택해줘.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus(statusTarget, `로컬 다운로드 감시 등록 중: ${selected.length}곡 ${format.toUpperCase()}`);
|
||||||
|
const response = await fetch("http://127.0.0.1:17873/expect-downloads", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
pageUrl: location.href,
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
format,
|
||||||
|
tracks: selected,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) throw new Error(text || `${response.status} ${response.statusText}`);
|
||||||
|
const data = tryParseJson(text) || {};
|
||||||
|
const root = data.downloadsRoot ? ` / 감시 폴더: ${data.downloadsRoot}` : "";
|
||||||
|
setStatus(statusTarget, `${format.toUpperCase()} 수동 다운로드 대기 등록: 새 ${data.added || 0}곡, 이미 있음 ${data.existingRequested || 0}곡, 대기 ${data.pendingDownloads || 0}곡.${root} 이제 SUNO 원래 메뉴에서 ${format.toUpperCase()}를 직접 받아줘.`);
|
||||||
|
}
|
||||||
|
|
||||||
function trackOptionLabel(track, index) {
|
function trackOptionLabel(track, index) {
|
||||||
const title = track?.title || track?.id || "제목 없음";
|
const title = track?.title || track?.id || "제목 없음";
|
||||||
const shortId = track?.id ? String(track.id).slice(0, 8) : "";
|
const shortId = track?.id ? String(track.id).slice(0, 8) : "";
|
||||||
@@ -1779,6 +1842,7 @@
|
|||||||
if (!data || typeof data !== "object") return "상태 정보 없음";
|
if (!data || typeof data !== "object") return "상태 정보 없음";
|
||||||
const active = Number(data.activeCount || 0);
|
const active = Number(data.activeCount || 0);
|
||||||
const queued = Number(data.queued || 0);
|
const queued = Number(data.queued || 0);
|
||||||
|
const pendingDownloads = Number(data.pendingDownloads || 0);
|
||||||
const done = Number(data.done ?? ((data.completed || 0) + (data.failed || 0)));
|
const done = Number(data.done ?? ((data.completed || 0) + (data.failed || 0)));
|
||||||
const total = Number(data.total || 0);
|
const total = Number(data.total || 0);
|
||||||
const failed = Number(data.failed || 0);
|
const failed = Number(data.failed || 0);
|
||||||
@@ -1786,7 +1850,7 @@
|
|||||||
const workerText = data.maxWorkers ? `${active}/${data.maxWorkers}` : `${active}`;
|
const workerText = data.maxWorkers ? `${active}/${data.maxWorkers}` : `${active}`;
|
||||||
const title = data.active || "";
|
const title = data.active || "";
|
||||||
const titleText = title ? ` / 처리중: ${title}` : "";
|
const titleText = title ? ` / 처리중: ${title}` : "";
|
||||||
return `작업 ${done}/${total} (${percent}%), 실행 ${workerText}, 대기 ${queued}, 실패 ${failed}${titleText}`;
|
return `작업 ${done}/${total} (${percent}%), 실행 ${workerText}, 큐 ${queued}, 다운로드대기 ${pendingDownloads}, 실패 ${failed}${titleText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootSuno() {
|
async function bootSuno() {
|
||||||
@@ -1814,15 +1878,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cgs-track-list" data-cgs-track-list></div>
|
<div class="cgs-track-list" data-cgs-track-list></div>
|
||||||
<div class="cgs-row">
|
<div class="cgs-row">
|
||||||
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-mp3>선택 1곡 MP3</button>
|
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-mp3>선택 1곡 MP3 자동</button>
|
||||||
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-wav>선택 1곡 WAV(UI)</button>
|
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-wav>선택 1곡 WAV 대기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cgs-row">
|
<div class="cgs-row">
|
||||||
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-mp3>선택항목 MP3</button>
|
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-mp3>선택항목 MP3 자동</button>
|
||||||
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-wav>선택항목 WAV(UI)</button>
|
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-wav>선택항목 WAV 대기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cgs-row">
|
<div class="cgs-row">
|
||||||
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-mp3>SUNO UI MP3 클릭</button>
|
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-mp3>SUNO UI MP3 재시도</button>
|
||||||
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-wav>SUNO UI WAV 재시도</button>
|
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-wav>SUNO UI WAV 재시도</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cgs-row">
|
<div class="cgs-row">
|
||||||
@@ -1846,12 +1910,14 @@
|
|||||||
const slotSelect = body.querySelector("[data-cgs-slot]");
|
const slotSelect = body.querySelector("[data-cgs-slot]");
|
||||||
const trackList = body.querySelector("[data-cgs-track-list]");
|
const trackList = body.querySelector("[data-cgs-track-list]");
|
||||||
let libraryTracks = [];
|
let libraryTracks = [];
|
||||||
|
let visibleTrackIds = new Set();
|
||||||
const libraryTrackMap = new Map();
|
const libraryTrackMap = new Map();
|
||||||
const selectedTrackIds = new Set();
|
const selectedTrackIds = new Set();
|
||||||
let lastSelectedIndex = -1;
|
let lastSelectedIndex = -1;
|
||||||
let autoScanTimer = 0;
|
let autoScanTimer = 0;
|
||||||
|
|
||||||
const getSelectedTracks = () => libraryTracks.filter((track) => selectedTrackIds.has(track.id));
|
const getVisibleTracks = () => libraryTracks.filter((track) => visibleTrackIds.has(track.id));
|
||||||
|
const getSelectedTracks = () => getVisibleTracks().filter((track) => selectedTrackIds.has(track.id));
|
||||||
const mergeLibraryTracks = (tracks) => {
|
const mergeLibraryTracks = (tracks) => {
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
if (!track.id) continue;
|
if (!track.id) continue;
|
||||||
@@ -1865,24 +1931,25 @@
|
|||||||
};
|
};
|
||||||
const renderLibraryTrackList = () => {
|
const renderLibraryTrackList = () => {
|
||||||
try {
|
try {
|
||||||
|
const visibleTracks = getVisibleTracks();
|
||||||
for (const id of [...selectedTrackIds]) {
|
for (const id of [...selectedTrackIds]) {
|
||||||
if (!libraryTrackMap.has(id)) selectedTrackIds.delete(id);
|
if (!visibleTrackIds.has(id)) selectedTrackIds.delete(id);
|
||||||
}
|
}
|
||||||
const selectedCount = selectedTrackIds.size;
|
const selectedCount = selectedTrackIds.size;
|
||||||
selectionSummary.textContent = libraryTracks.length
|
selectionSummary.textContent = visibleTracks.length
|
||||||
? `누적 ${libraryTracks.length}곡 / 선택 ${selectedCount}곡`
|
? `현재 화면 ${visibleTracks.length}곡 / 선택 ${selectedCount}곡 / 누적 ${libraryTracks.length}곡`
|
||||||
: "누적된 곡 없음. 스크롤하면서 곡 감지를 눌러줘.";
|
: `현재 화면 감지 없음 / 누적 ${libraryTracks.length}곡`;
|
||||||
|
|
||||||
const nodes = [];
|
const nodes = [];
|
||||||
if (!libraryTracks.length) {
|
if (!visibleTracks.length) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "cgs-track-item";
|
empty.className = "cgs-track-item";
|
||||||
empty.innerHTML = `<div></div><div class="cgs-track-meta">감지된 곡 없음</div><div></div>`;
|
empty.innerHTML = `<div></div><div class="cgs-track-meta">현재 화면에 감지된 곡 없음</div><div></div>`;
|
||||||
trackList.replaceChildren(empty);
|
trackList.replaceChildren(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryTracks.forEach((track, index) => {
|
visibleTracks.forEach((track, index) => {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = `cgs-track-item ${selectedTrackIds.has(track.id) ? "cgs-selected" : ""}`;
|
row.className = `cgs-track-item ${selectedTrackIds.has(track.id) ? "cgs-selected" : ""}`;
|
||||||
row.dataset.cgsTrackIndex = String(index);
|
row.dataset.cgsTrackIndex = String(index);
|
||||||
@@ -1920,15 +1987,17 @@
|
|||||||
};
|
};
|
||||||
const setSelectedRange = (from, to) => {
|
const setSelectedRange = (from, to) => {
|
||||||
const start = Math.max(0, Math.min(from, to));
|
const start = Math.max(0, Math.min(from, to));
|
||||||
const end = Math.min(libraryTracks.length - 1, Math.max(from, to));
|
const visibleTracks = getVisibleTracks();
|
||||||
|
const end = Math.min(visibleTracks.length - 1, Math.max(from, to));
|
||||||
for (let index = start; index <= end; index += 1) {
|
for (let index = start; index <= end; index += 1) {
|
||||||
if (libraryTracks[index]?.id) selectedTrackIds.add(libraryTracks[index].id);
|
if (visibleTracks[index]?.id) selectedTrackIds.add(visibleTracks[index].id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const scanAndMergeVisibleTracks = () => {
|
const scanAndMergeVisibleTracks = () => {
|
||||||
const scannedTracks = scanSunoLibraryTracks();
|
const scannedTracks = scanSunoLibraryTracks();
|
||||||
const before = libraryTracks.length;
|
const before = libraryTracks.length;
|
||||||
mergeLibraryTracks(scannedTracks);
|
mergeLibraryTracks(scannedTracks);
|
||||||
|
visibleTrackIds = new Set(scannedTracks.map((track) => track.id).filter(Boolean));
|
||||||
renderLibraryTrackList();
|
renderLibraryTrackList();
|
||||||
updateSummary();
|
updateSummary();
|
||||||
return { scannedTracks, added: libraryTracks.length - before };
|
return { scannedTracks, added: libraryTracks.length - before };
|
||||||
@@ -1998,7 +2067,8 @@
|
|||||||
const item = event.target.closest("[data-cgs-track-index]");
|
const item = event.target.closest("[data-cgs-track-index]");
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
const index = Number(item.getAttribute("data-cgs-track-index"));
|
const index = Number(item.getAttribute("data-cgs-track-index"));
|
||||||
const track = libraryTracks[index];
|
const visibleTracks = getVisibleTracks();
|
||||||
|
const track = visibleTracks[index];
|
||||||
if (!track?.id) return;
|
if (!track?.id) return;
|
||||||
if (event.shiftKey && lastSelectedIndex >= 0) {
|
if (event.shiftKey && lastSelectedIndex >= 0) {
|
||||||
setSelectedRange(lastSelectedIndex, index);
|
setSelectedRange(lastSelectedIndex, index);
|
||||||
@@ -2021,7 +2091,7 @@
|
|||||||
renderLibraryTrackList();
|
renderLibraryTrackList();
|
||||||
}
|
}
|
||||||
updateSummary();
|
updateSummary();
|
||||||
const track = getSelectedTracks()[0] || libraryTracks[0];
|
const track = getSelectedTracks()[0] || getVisibleTracks()[0];
|
||||||
if (!track) {
|
if (!track) {
|
||||||
setStatus(body, "선택할 감지 곡이 없어. 곡 감지를 먼저 눌러줘.");
|
setStatus(body, "선택할 감지 곡이 없어. 곡 감지를 먼저 눌러줘.");
|
||||||
return;
|
return;
|
||||||
@@ -2039,7 +2109,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-process-one-wav]").addEventListener("click", async () => {
|
body.querySelector("[data-cgs-process-one-wav]").addEventListener("click", async () => {
|
||||||
await clickSelectedSunoUiDownloads("wav", { one: true });
|
await registerSelectedManualDownloads("wav", { one: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-process-selected-mp3]").addEventListener("click", async () => {
|
body.querySelector("[data-cgs-process-selected-mp3]").addEventListener("click", async () => {
|
||||||
@@ -2047,11 +2117,28 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-process-selected-wav]").addEventListener("click", async () => {
|
body.querySelector("[data-cgs-process-selected-wav]").addEventListener("click", async () => {
|
||||||
await clickSelectedSunoUiDownloads("wav");
|
await registerSelectedManualDownloads("wav");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const registerSelectedManualDownloads = async (format, options = {}) => {
|
||||||
|
if (!getVisibleTracks().length) {
|
||||||
|
mergeLibraryTracks(scanSunoLibraryTracks());
|
||||||
|
renderLibraryTrackList();
|
||||||
|
}
|
||||||
|
const tracks = options.one ? (getSelectedTracks().slice(0, 1)) : getSelectedTracks();
|
||||||
|
if (!tracks.length) {
|
||||||
|
setStatus(body, "선택된 곡이 없어. 목록에서 받을 곡을 먼저 선택해줘.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await expectManualDownloads(tracks, body, format);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus(body, `${format.toUpperCase()} 다운로드 대기 등록 실패: ${error.message || error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const clickSelectedSunoUiDownloads = async (format, options = {}) => {
|
const clickSelectedSunoUiDownloads = async (format, options = {}) => {
|
||||||
if (!libraryTracks.length) {
|
if (!getVisibleTracks().length) {
|
||||||
mergeLibraryTracks(scanSunoLibraryTracks());
|
mergeLibraryTracks(scanSunoLibraryTracks());
|
||||||
renderLibraryTrackList();
|
renderLibraryTrackList();
|
||||||
}
|
}
|
||||||
@@ -2084,20 +2171,24 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const processLibraryTracks = async (format) => {
|
const processLibraryTracks = async (format) => {
|
||||||
if (!libraryTracks.length) {
|
if (!getVisibleTracks().length) {
|
||||||
mergeLibraryTracks(scanSunoLibraryTracks());
|
mergeLibraryTracks(scanSunoLibraryTracks());
|
||||||
renderLibraryTrackList();
|
renderLibraryTrackList();
|
||||||
}
|
}
|
||||||
updateSummary();
|
updateSummary();
|
||||||
try {
|
try {
|
||||||
await uploadTracksToCompanion(libraryTracks, body, format);
|
if (format === "wav") {
|
||||||
|
await expectManualDownloads(libraryTracks, body, "wav");
|
||||||
|
} else {
|
||||||
|
await uploadTracksToCompanion(libraryTracks, body, format);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(body, `${format.toUpperCase()} 일괄 처리 실패: ${error.message || error}`);
|
setStatus(body, `${format.toUpperCase()} 일괄 처리 실패: ${error.message || error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processSelectedTracks = async (format) => {
|
const processSelectedTracks = async (format) => {
|
||||||
if (!libraryTracks.length) {
|
if (!getVisibleTracks().length) {
|
||||||
mergeLibraryTracks(scanSunoLibraryTracks());
|
mergeLibraryTracks(scanSunoLibraryTracks());
|
||||||
renderLibraryTrackList();
|
renderLibraryTrackList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,7 +350,6 @@ function Write-AlbumNfo {
|
|||||||
$lines.Add(" <dateadded>$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</dateadded>")
|
$lines.Add(" <dateadded>$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')</dateadded>")
|
||||||
$lines.Add(" <title>$(Escape-XmlText $AlbumTitle)</title>")
|
$lines.Add(" <title>$(Escape-XmlText $AlbumTitle)</title>")
|
||||||
if ($runtime -gt 0) { $lines.Add(" <runtime>$runtime</runtime>") }
|
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(" <artist>$(Escape-XmlText $Artist)</artist>")
|
||||||
$lines.Add(" <albumartist>$(Escape-XmlText $Artist)</albumartist>")
|
$lines.Add(" <albumartist>$(Escape-XmlText $Artist)</albumartist>")
|
||||||
foreach ($line in $trackXml) { $lines.Add($line) }
|
foreach ($line in $trackXml) { $lines.Add($line) }
|
||||||
@@ -513,15 +512,8 @@ foreach ($track in $tracks) {
|
|||||||
Add-MetadataArg $argsList "ARTISTS" $ArtistName
|
Add-MetadataArg $argsList "ARTISTS" $ArtistName
|
||||||
Add-MetadataArg $argsList "track" "$trackNo"
|
Add-MetadataArg $argsList "track" "$trackNo"
|
||||||
Add-MetadataArg $argsList "disc" "1/1"
|
Add-MetadataArg $argsList "disc" "1/1"
|
||||||
Add-MetadataArg $argsList "GENRE" $style
|
|
||||||
Add-MetadataArg $argsList "lyrics-XXX" $lyrics
|
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 "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($outputFile)
|
$argsList.Add($outputFile)
|
||||||
|
|
||||||
& $ffmpeg @argsList
|
& $ffmpeg @argsList
|
||||||
@@ -531,7 +523,7 @@ foreach ($track in $tracks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "완료: $outputFile"
|
Write-Host "완료: $outputFile"
|
||||||
Write-AlbumNfo $trackFolder $albumTitle $ArtistName $style $ffprobe
|
Write-AlbumNfo $trackFolder $albumTitle $ArtistName "" $ffprobe
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $KeepWorkFiles -and (Test-Path -LiteralPath $workRoot)) {
|
if (-not $KeepWorkFiles -and (Test-Path -LiteralPath $workRoot)) {
|
||||||
|
|||||||
+109
-1
@@ -17,6 +17,8 @@ $queue = [System.Collections.Queue]::new()
|
|||||||
$queueIds = [System.Collections.Generic.HashSet[string]]::new()
|
$queueIds = [System.Collections.Generic.HashSet[string]]::new()
|
||||||
$activeJobs = [System.Collections.ArrayList]::new()
|
$activeJobs = [System.Collections.ArrayList]::new()
|
||||||
$activeIds = [System.Collections.Generic.HashSet[string]]::new()
|
$activeIds = [System.Collections.Generic.HashSet[string]]::new()
|
||||||
|
$pendingDownloads = [System.Collections.ArrayList]::new()
|
||||||
|
$claimedDownloadPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
$stats = [ordered]@{
|
$stats = [ordered]@{
|
||||||
total = 0
|
total = 0
|
||||||
completed = 0
|
completed = 0
|
||||||
@@ -238,6 +240,7 @@ function Get-QueueSnapshot {
|
|||||||
return [pscustomobject]@{
|
return [pscustomobject]@{
|
||||||
maxWorkers = $MaxWorkers
|
maxWorkers = $MaxWorkers
|
||||||
queued = $script:queue.Count
|
queued = $script:queue.Count
|
||||||
|
pendingDownloads = $script:pendingDownloads.Count
|
||||||
activeCount = $active.Count
|
activeCount = $active.Count
|
||||||
active = if ($active.Count) { ($active | Select-Object -First 1).title } else { "" }
|
active = if ($active.Count) { ($active | Select-Object -First 1).title } else { "" }
|
||||||
activeJobs = $active
|
activeJobs = $active
|
||||||
@@ -302,7 +305,7 @@ function Show-Dashboard {
|
|||||||
Write-Host "Library: $libraryRoot"
|
Write-Host "Library: $libraryRoot"
|
||||||
Write-Host "Logs: $logs"
|
Write-Host "Logs: $logs"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host ("Workers: {0}/{1} Queue: {2} Done: {3}/{4} Failed: {5} Progress: {6}%" -f $snapshot.activeCount, $snapshot.maxWorkers, $snapshot.queued, $snapshot.done, $snapshot.total, $snapshot.failed, $snapshot.percent)
|
Write-Host ("Workers: {0}/{1} Queue: {2} Waiting downloads: {3} Done: {4}/{5} Failed: {6} Progress: {7}%" -f $snapshot.activeCount, $snapshot.maxWorkers, $snapshot.queued, $snapshot.pendingDownloads, $snapshot.done, $snapshot.total, $snapshot.failed, $snapshot.percent)
|
||||||
Write-Host ("{0} {1}/{2}" -f (Format-ProgressBar $snapshot.done $snapshot.total), $snapshot.done, $snapshot.total) -ForegroundColor Cyan
|
Write-Host ("{0} {1}/{2}" -f (Format-ProgressBar $snapshot.done $snapshot.total), $snapshot.done, $snapshot.total) -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
@@ -439,6 +442,93 @@ function Save-UploadedTrack {
|
|||||||
return Add-TracksToQueue @($track) "$($Payload.pageUrl)"
|
return Add-TracksToQueue @($track) "$($Payload.pageUrl)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-DownloadsRoot {
|
||||||
|
$path = Join-Path $env:USERPROFILE "Downloads"
|
||||||
|
if (Test-Path -LiteralPath $path) { return $path }
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-StableDownloadFile {
|
||||||
|
param([System.IO.FileInfo]$File, [string]$Ext)
|
||||||
|
|
||||||
|
if (-not $File -or -not $File.Exists) { return $false }
|
||||||
|
if ($File.Name -like "*.crdownload" -or $File.Name -like "*.tmp") { return $false }
|
||||||
|
if ($File.Extension.TrimStart(".").ToLowerInvariant() -ne $Ext) { return $false }
|
||||||
|
if ($File.Length -lt 131072) { return $false }
|
||||||
|
return ((Get-Date) - $File.LastWriteTime).TotalSeconds -ge 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-ExpectedDownloadFile {
|
||||||
|
param($Expected)
|
||||||
|
|
||||||
|
$downloads = Get-DownloadsRoot
|
||||||
|
if (-not $downloads) { return $null }
|
||||||
|
$ext = "$($Expected.Format)".TrimStart(".").ToLowerInvariant()
|
||||||
|
$registeredAt = [DateTime]$Expected.RegisteredAt
|
||||||
|
$candidates = Get-ChildItem -LiteralPath $downloads -File -Filter "*.$ext" -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object {
|
||||||
|
-not $script:claimedDownloadPaths.Contains($_.FullName) -and
|
||||||
|
$_.CreationTime -ge $registeredAt.AddSeconds(-10) -and
|
||||||
|
(Test-StableDownloadFile $_ $ext)
|
||||||
|
} |
|
||||||
|
Sort-Object CreationTime
|
||||||
|
|
||||||
|
return @($candidates)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function Register-ExpectedDownloads {
|
||||||
|
param($Payload)
|
||||||
|
|
||||||
|
$format = "$($Payload.format)".TrimStart(".").ToLowerInvariant()
|
||||||
|
if ($format -notin @("mp3", "wav")) { throw "format must be mp3 or wav" }
|
||||||
|
$tracks = @($Payload.tracks)
|
||||||
|
if (-not $tracks.Count) { throw "tracks is empty" }
|
||||||
|
|
||||||
|
$missing = Get-MissingTracks $tracks
|
||||||
|
$added = 0
|
||||||
|
foreach ($track in @($missing.missing)) {
|
||||||
|
$id = "$($track.id)".Trim()
|
||||||
|
if (-not $id) { continue }
|
||||||
|
$alreadyPending = @($script:pendingDownloads | Where-Object { $_.Id -eq $id -and $_.Format -eq $format }).Count -gt 0
|
||||||
|
if ($alreadyPending -or $script:queueIds.Contains($id) -or $script:activeIds.Contains($id)) { continue }
|
||||||
|
[void]$script:pendingDownloads.Add([pscustomobject]@{
|
||||||
|
Id = $id
|
||||||
|
Title = "$($track.title)"
|
||||||
|
Track = $track
|
||||||
|
Format = $format
|
||||||
|
PageUrl = "$($Payload.pageUrl)"
|
||||||
|
RegisteredAt = Get-Date
|
||||||
|
})
|
||||||
|
$added++
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
added = $added
|
||||||
|
pending = $script:pendingDownloads.Count
|
||||||
|
existingRequested = @($missing.existingRequested).Count
|
||||||
|
missingRequested = @($missing.missing).Count
|
||||||
|
invalidRequested = $missing.invalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Update-ExpectedDownloads {
|
||||||
|
if (-not $script:pendingDownloads.Count) { return }
|
||||||
|
|
||||||
|
for ($index = $script:pendingDownloads.Count - 1; $index -ge 0; $index--) {
|
||||||
|
$expected = $script:pendingDownloads[$index]
|
||||||
|
$file = Find-ExpectedDownloadFile $expected
|
||||||
|
if (-not $file) { continue }
|
||||||
|
|
||||||
|
[void]$script:claimedDownloadPaths.Add($file.FullName)
|
||||||
|
$track = $expected.Track
|
||||||
|
$track | Add-Member -NotePropertyName "localAudioPath" -NotePropertyValue $file.FullName -Force
|
||||||
|
$track | Add-Member -NotePropertyName "localAudioExt" -NotePropertyValue $expected.Format -Force
|
||||||
|
$result = Add-TracksToQueue @($track) "$($expected.PageUrl)"
|
||||||
|
$script:pendingDownloads.RemoveAt($index)
|
||||||
|
Write-Host "Picked downloaded $($expected.Format): $($expected.Title) <- $($file.Name) queued=$($result.queued)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-ExistingSunoIds {
|
function Get-ExistingSunoIds {
|
||||||
param([string]$Root)
|
param([string]$Root)
|
||||||
|
|
||||||
@@ -501,6 +591,7 @@ Show-Dashboard -Force
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
while ($true) {
|
while ($true) {
|
||||||
|
Update-ExpectedDownloads
|
||||||
Update-Queue
|
Update-Queue
|
||||||
if (-not $listener.Pending()) {
|
if (-not $listener.Pending()) {
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 500
|
||||||
@@ -526,6 +617,7 @@ try {
|
|||||||
artist = $ArtistName
|
artist = $ArtistName
|
||||||
queued = $snapshot.queued
|
queued = $snapshot.queued
|
||||||
active = $snapshot.active
|
active = $snapshot.active
|
||||||
|
pendingDownloads = $snapshot.pendingDownloads
|
||||||
activeCount = $snapshot.activeCount
|
activeCount = $snapshot.activeCount
|
||||||
activeJobs = $snapshot.activeJobs
|
activeJobs = $snapshot.activeJobs
|
||||||
next = $snapshot.next
|
next = $snapshot.next
|
||||||
@@ -581,6 +673,22 @@ try {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request.Method -eq "POST" -and $request.Path -eq "/expect-downloads") {
|
||||||
|
$payload = $request.Body | ConvertFrom-Json
|
||||||
|
$result = Register-ExpectedDownloads $payload
|
||||||
|
Write-Host "Waiting for browser downloads: added=$($result.added), pending=$($result.pending), format=$($payload.format)"
|
||||||
|
Write-Response $stream 200 "OK" (@{
|
||||||
|
ok = $true
|
||||||
|
added = $result.added
|
||||||
|
pendingDownloads = $result.pending
|
||||||
|
existingRequested = $result.existingRequested
|
||||||
|
missingRequested = $result.missingRequested
|
||||||
|
invalidRequested = $result.invalidRequested
|
||||||
|
downloadsRoot = Get-DownloadsRoot
|
||||||
|
} | ConvertTo-Json -Compress)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if ($request.Method -eq "POST" -and $request.Path -eq "/process") {
|
if ($request.Method -eq "POST" -and $request.Path -eq "/process") {
|
||||||
$manifest = $request.Body | ConvertFrom-Json
|
$manifest = $request.Body | ConvertFrom-Json
|
||||||
if (-not $manifest.tracks -or @($manifest.tracks).Count -eq 0) {
|
if (-not $manifest.tracks -or @($manifest.tracks).Count -eq 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user