Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions apps/desktop/src/lib/analysis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { importYoutubeUrl } from "./analysis";

type TauriWindow = Window & {
__TAURI_INTERNALS__?: unknown;
__TAURI_INVOKE__?: (command: string, args?: Record<string, unknown>) => Promise<unknown>;
};

const tauriWindow = window as TauriWindow;

describe("analysis bridge", () => {
beforeEach(() => {
delete tauriWindow.__TAURI_INTERNALS__;
delete tauriWindow.__TAURI_INVOKE__;
});

it("imports a standard YouTube URL through the browser fallback when Tauri is absent", async () => {
const selection = await importYoutubeUrl("https://www.youtube.com/watch?v=4ozX4yFUC34");

expect(selection).toEqual({
ok: true,
bootstrap: {
projectId: "browser-youtube-project",
sourceMode: "reference",
projectRoot: "browser://bandscope/projects/browser-youtube-project",
cacheRoot: "browser://bandscope/cache/browser-youtube-project",
tempRoot: "browser://bandscope/temp/browser-youtube-project",
source: {
sourcePath: "browser://bandscope/temp/browser-youtube-project/youtube-preview.m4a",
fileName: "youtube-preview.m4a",
extension: "m4a",
fileSizeBytes: 1
}
}
});
});

it("uses the browser fallback when Tauri internals are present but invoke is unavailable", async () => {
tauriWindow.__TAURI_INTERNALS__ = {};

const selection = await importYoutubeUrl("https://www.youtube.com/watch?v=4ozX4yFUC34");

expect(selection.ok).toBe(true);
});

it("keeps browser fallback URL intake aligned with the native YouTube allowlist", async () => {
const selection = await importYoutubeUrl("https://example.com/watch?v=4ozX4yFUC34");

expect(selection).toEqual({
ok: false,
error: {
code: "invalid_request",
message: "Only standard YouTube URLs are supported."
}
});
});

it("uses the Tauri v1 invoke shim when it is available", async () => {
tauriWindow.__TAURI_INVOKE__ = vi.fn().mockResolvedValue({
projectId: "native-youtube-project",
sourceMode: "reference",
projectRoot: "/tmp/bandscope/projects/native-youtube-project",
cacheRoot: "/tmp/bandscope/cache/native-youtube-project",
tempRoot: "/tmp/bandscope/temp/native-youtube-project",
source: {
sourcePath: "/tmp/bandscope/temp/native-youtube-project/youtube.wav",
fileName: "youtube.wav",
extension: "wav",
fileSizeBytes: 1024
}
});

const selection = await importYoutubeUrl("https://youtu.be/4ozX4yFUC34");

expect(tauriWindow.__TAURI_INVOKE__).toHaveBeenCalledWith("import_youtube_url", {
url: "https://youtu.be/4ozX4yFUC34"
});
expect(selection.ok).toBe(true);
});
});
70 changes: 68 additions & 2 deletions apps/desktop/src/lib/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createAnalysisJobStatus,
createDemoAnalysisJobRequest,
createDemoRehearsalSong,
createProjectBootstrapSummary,
isAnalysisJobStatus,
parseAnalysisJobRequest,
parseProjectBootstrapSummary,
Expand All @@ -18,6 +19,9 @@ type TauriInvoke = (command: string, args?: Record<string, unknown>) => Promise<

declare global {
interface Window {
__TAURI_INTERNALS__?: {
invoke?: unknown;
};
__TAURI_INVOKE__?: TauriInvoke;
}
}
Expand All @@ -43,7 +47,49 @@ function getInvoke(): TauriInvoke | null {
return null;
}

return window.__TAURI_INVOKE__ ?? invoke;
// Detect Tauri v2 only when its invoke bridge is actually available.
const tauriInternals = window.__TAURI_INTERNALS__;
if (tauriInternals && typeof tauriInternals.invoke === "function") {
return invoke;
}

// Detect the legacy test/dev shim.
if (window.__TAURI_INVOKE__) {
return window.__TAURI_INVOKE__;
}

return null;
}

/** Documented. */
function isSupportedYoutubeUrl(rawUrl: unknown): rawUrl is string {
if (typeof rawUrl !== "string") {
return false;
}

let parsedUrl: URL;
try {
parsedUrl = new URL(rawUrl);
} catch {
return false;
}

if (parsedUrl.protocol !== "https:") {
return false;
}

const host = parsedUrl.hostname.toLowerCase();
if (host === "youtu.be") {
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
return pathSegments.length === 1;
}

if (host === "youtube.com" || host.endsWith(".youtube.com")) {
const videoIds = parsedUrl.searchParams.getAll("v").filter((value) => value.trim().length > 0);
return parsedUrl.pathname === "/watch" && videoIds.length === 1;
}

return false;
}

/** Documented. */
Expand Down Expand Up @@ -97,6 +143,26 @@ async function browserFallback(command: string, args?: Record<string, unknown>):
return;
}

if (command === "import_youtube_url") {
if (!isSupportedYoutubeUrl(args?.url)) {
throw new Error("Only standard YouTube URLs are supported.");
}

const projectId = "browser-youtube-project";
return createProjectBootstrapSummary({
projectId,
projectRoot: `browser://bandscope/projects/${projectId}`,
cacheRoot: `browser://bandscope/cache/${projectId}`,
tempRoot: `browser://bandscope/temp/${projectId}`,
source: {
sourcePath: `browser://bandscope/temp/${projectId}/youtube-preview.m4a`,
fileName: "youtube-preview.m4a",
extension: "m4a",
fileSizeBytes: 1
}
});
}

if (command === "load_project") {
throw new Error("Local load not supported in browser");
}
Expand Down Expand Up @@ -184,7 +250,7 @@ export async function importYoutubeUrl(url: string): Promise<LocalAudioSelection
bootstrap: parseProjectBootstrapSummary(response)
};
} catch (error) {
const message = error instanceof Error ? error.message : (typeof error === 'string' ? error : "YouTube import failed.");
const message = error instanceof Error ? error.message : (typeof error === "string" ? error : "YouTube import failed.");
return {
ok: false,
error: {
Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"appTitle": "BandScope Bootstrap",
"appSubtitle": "GitHub-governed local-first desktop analysis baseline",
"homeCard": "Home baseline is wired.",
"playerCard": "Player baseline is wired.",
"chordsCard": "Chord analysis baseline is wired.",
"rangesCard": "Range analysis baseline is wired.",
"settingsCard": "Settings baseline is wired.",
"appTitle": "BandScope",
"appSubtitle": "Local-first desktop analysis tool for rehearsal prep",
"homeCard": "Home functionality is ready.",
"playerCard": "Player functionality is ready.",
"chordsCard": "Chord analysis functionality is ready.",
"rangesCard": "Range analysis functionality is ready.",
"settingsCard": "Settings functionality is ready.",
"supportedFormats": "Supported input formats",
"chooseLocalAudio": "Choose local audio",
"selectedAudio": "Selected audio",
Expand Down
14 changes: 7 additions & 7 deletions apps/desktop/src/locales/ko/common.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"appTitle": "BandScope 부트스트랩",
"appSubtitle": "GitHub 거버넌스를 갖춘 로컬 우선 데스크톱 분석 기준선",
"homeCard": "홈 기준선이 연결되었습니다.",
"playerCard": "플레이어 기준선이 연결되었습니다.",
"chordsCard": "코드 분석 기준선이 연결되었습니다.",
"rangesCard": "음역 분석 기준선이 연결되었습니다.",
"settingsCard": "설정 기준선이 연결되었습니다.",
"appTitle": "BandScope",
"appSubtitle": "합주 준비를 위한 로컬-퍼스트 분석 도구",
"homeCard": "홈 기능이 준비되었습니다.",
"playerCard": "플레이어 기능이 준비되었습니다.",
"chordsCard": "코드 분석 기능이 준비되었습니다.",
"rangesCard": "음역 분석 기능이 준비되었습니다.",
"settingsCard": "설정 기능이 준비되었습니다.",
"supportedFormats": "지원 입력 형식",
"chooseLocalAudio": "로컬 오디오 선택",
"selectedAudio": "선택한 오디오",
Expand Down
Loading