Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ VITE_WEB_PORT=14558
VITE_SERVER_PORT=25884
# API 地址 - 结尾不要加 /
VITE_API_URL=/api/netease
# Google Drive
GOOGLE_DRIVE_CLIENT_ID=
GOOGLE_DRIVE_CLIENT_SECRET=
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
AddRemoteFolderModal: typeof import('./src/components/Modal/AddRemoteFolderModal.vue')['default']
AMLLServer: typeof import('./src/components/Modal/Setting/AMLLServer.vue')['default']
AMLyric: typeof import('./src/components/Player/PlayerLyric/AMLyric.vue')['default']
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
Expand Down
4 changes: 4 additions & 0 deletions electron/main/ipc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import initSocketIpc from "./ipc-socket";
import initMediaIpc from "./ipc-media";
import initMpvIpc from "./ipc-mpv";
import initRendererLogIpc from "./ipc-renderer-log";
import { registerRemoteFolderIpc } from "./ipc-remote-folder";
import initGoogleDriveIpc from "./ipc-google-drive";

/**
* 初始化全部 IPC 通信
Expand All @@ -34,6 +36,8 @@ const initIpc = (): void => {
initMediaIpc();
initMpvIpc();
initRendererLogIpc();
registerRemoteFolderIpc();
initGoogleDriveIpc();
};

export default initIpc;
126 changes: 124 additions & 2 deletions electron/main/ipc/ipc-file.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
import { access, mkdir, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
import { parseFile } from "music-metadata";
import { parseFile, parseStream as parseFileFromStream } from "music-metadata";
import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper";
import { File, Picture, Id3v2Settings, TagTypes } from "node-taglib-sharp";
import { ipcLog } from "../logger";
import { GoogleDriveService } from "../services/GoogleDriveService";
import { ipcLog, processLog } from "../logger";
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { Options as GlobOptions } from "fast-glob/out/settings";
Expand Down Expand Up @@ -58,6 +59,47 @@ const initFileIpc = (): void => {
const store = useStore();
const localCachePath = join(store.get("cachePath"), "local-data");
const coverDir = join(localCachePath, "covers");

// --- Google Drive 集成 ---
try {
const driveService = GoogleDriveService.getInstance();
if (driveService.hasValidTokens()) {
processLog.info("☁️ Fetching Google Drive audio files...");
const driveFiles = await driveService.getAudioFiles();

const driveTracks = driveFiles.map(file => ({
id: file.id || "",
path: `google-drive://${file.id}`,
// 去除扩展名作为标题
title: file.name?.replace(/\.[^/.]+$/, "") || "Unknown Title",
artist: "Google Drive",
album: "Cloud Drive",
// 暂无时长信息
duration: 0,
size: parseInt(file.size || "0"),
mtime: file.modifiedTime ? new Date(file.modifiedTime).getTime() : Date.now(),
cover: undefined,
bitrate: 0
}));

// 发送 Drive 数据批次
event.sender.send("music-sync-tracks-batch", driveTracks);
processLog.info(`☁️ Sent ${driveTracks.length} Google Drive tracks`);
}
} catch (error: any) {
processLog.error("❌ Failed to sync Google Drive files:", error);
// 如果是权限不足 (Permission denied),发送明确提示
if (error.message && (error.message.includes("403") || error.message.includes("insufficient"))) {
event.sender.send("music-sync-complete", {
success: false,
message: "Google Drive 权限不足,请在目录管理中重新连接以授权"
});
return { success: false, message: "Google Drive Auth Error" };
}
}
// ------------------------

// 使用批量流式传输,减少 IPC 通信次数

// 使用批量流式传输,减少 IPC 通信次数
await localMusicService.refreshLibrary(
Expand Down Expand Up @@ -175,6 +217,45 @@ const initFileIpc = (): void => {
// 获取音乐元信息
ipcMain.handle("get-music-metadata", async (_, path: string) => {
try {
if (path.startsWith("google-drive://")) {
const fileId = path.replace("google-drive://", "");
try {
const driveService = GoogleDriveService.getInstance();

// 并行获取:文件流(用于解析 tag)和 API 元数据(用于获取准确大小)
const [metadata, streamReuslt] = await Promise.all([
driveService.getFileMetadata(fileId),
driveService.getFileStream(fileId),
]);

const { data } = streamReuslt;
// 解析流
const { common, format } = await parseFileFromStream(data);

return {
fileName: common.title || metadata.name || "Unknown Title",
fileSize: metadata.size / (1024 * 1024), // 转换为 MB
common,
lyric:
metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) ||
common?.lyrics?.[0]?.text ||
"",
format,
md5: "",
};
} catch (e) {
processLog.error(`⚠️ Failed to parse Google Drive metadata for ${fileId}:`, e);
// Fallback
return {
fileName: "Google Drive File",
fileSize: 0,
common: { title: "Cloud Song", artist: "Google Drive" },
lyric: "",
format: {},
md5: "",
};
}
}
const filePath = resolve(path).replace(/\\/g, "/");
const { common, format } = await parseFile(filePath);
return {
Expand Down Expand Up @@ -243,6 +324,32 @@ const initFileIpc = (): void => {
format: "lrc" | "ttml";
}> => {
try {
// Google Drive 逻辑
if (musicPath.startsWith("google-drive://")) {
const fileId = musicPath.replace("google-drive://", "");
try {
const driveService = GoogleDriveService.getInstance();
const { data } = await driveService.getFileStream(fileId);
const { common } = await parseFileFromStream(data);

const syncedLyric = common?.lyrics?.[0]?.syncText;
if (syncedLyric && syncedLyric.length > 0) {
return {
lyric: metaDataLyricsArrayToLrc(syncedLyric),
format: "lrc",
};
} else if (common?.lyrics?.[0]?.text) {
return {
lyric: common?.lyrics?.[0]?.text,
format: "lrc",
};
}
} catch (e) {
processLog.error(`⚠️ Failed to parse lyrics from Drive file ${fileId}:`, e);
}
return { lyric: "", format: "lrc" };
}

// 获取文件基本信息
const absPath = resolve(musicPath);
const dir = dirname(absPath);
Expand Down Expand Up @@ -305,6 +412,21 @@ const initFileIpc = (): void => {
"get-music-cover",
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
try {
if (path.startsWith("google-drive://")) {
const fileId = path.replace("google-drive://", "");
try {
const driveService = GoogleDriveService.getInstance();
const { data } = await driveService.getFileStream(fileId);
const { common } = await parseFileFromStream(data);
const picture = common.picture?.[0];
if (picture) {
return { data: Buffer.from(picture.data), format: picture.format };
}
} catch (e) {
processLog.error(`⚠️ Failed to parse cover from Drive file ${fileId}:`, e);
}
return null;
}
const { common } = await parseFile(path);
// 获取封面数据
const picture = common.picture?.[0];
Expand Down
102 changes: 102 additions & 0 deletions electron/main/ipc/ipc-google-drive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { ipcMain } from "electron";
import { GoogleDriveService } from "../services/GoogleDriveService";
import { processLog } from "../logger";

/**
* 初始化 Google Drive IPC 通信
*/
const initGoogleDriveIpc = (): void => {
processLog.info("🔌 Initializing Google Drive IPC handlers");

// 触发 Google 登录
ipcMain.handle("google-drive-login", async () => {
try {
const service = GoogleDriveService.getInstance();
const result = await service.startAuth();
return result;
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive login failed:", err);
return { status: "error", message: err.message };
}
});

// 获取认证状态
ipcMain.handle("google-drive-status", () => {
try {
const service = GoogleDriveService.getInstance();
return service.getAuthStatus();
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive status check failed:", err);
return { authenticated: false, hasRefreshToken: false };
}
});

// 获取文件列表
ipcMain.handle("google-drive-files", async (_event, pageSize = 10) => {
try {
const service = GoogleDriveService.getInstance();
const files = await service.getFiles(pageSize);
return { status: "success", files };
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive files fetch failed:", err);
return { status: "error", message: err.message, files: [] };
}
});

// 登出
ipcMain.handle("google-drive-logout", () => {
try {
const service = GoogleDriveService.getInstance();
service.logout();
return { status: "success" };
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive logout failed:", err);
return { status: "error", message: err.message };
}
});

// 扫描音频文件
ipcMain.handle("google-drive-scan", async (_event, pageSize = 1000) => {
try {
const service = GoogleDriveService.getInstance();
const files = await service.getAudioFiles(pageSize);
return { status: "success", files };
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive scan failed:", err);
return { status: "error", message: err.message, files: [] };
}
});

// 获取流式播放信息
ipcMain.handle("google-drive-stream-info", async (_event, fileId: string) => {
try {
const service = GoogleDriveService.getInstance();
const info = await service.getStreamInfo(fileId);
return { status: "success", info };
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive stream info failed:", err);
return { status: "error", message: err.message };
}
});

// 下载文件
ipcMain.handle("google-drive-download", async (_event, fileId: string, destPath: string) => {
try {
const service = GoogleDriveService.getInstance();
await service.downloadFile(fileId, destPath);
return { status: "success" };
} catch (error) {
const err = error as Error;
processLog.error("❌ Google Drive download failed:", err);
return { status: "error", message: err.message };
}
});
};

export default initGoogleDriveIpc;
36 changes: 36 additions & 0 deletions electron/main/ipc/ipc-remote-folder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ipcMain } from "electron";
import { RemoteFolderService, type TestConnectionParams } from "../services/RemoteFolderService";

const remoteFolderService = RemoteFolderService.getInstance();

/**
* 注册远程文件夹相关的 IPC 处理器
*/
export const registerRemoteFolderIpc = () => {
/**
* 测试远程文件夹连接
*/
ipcMain.handle("remote-folder-test", async (_event, params: TestConnectionParams) => {
try {
const result = await remoteFolderService.testConnection(params);
return result;
} catch (error) {
return {
success: false,
message: `测试连接失败: ${(error as Error).message}`,
};
}
});

/**
* 检查远程路径是否可访问
*/
ipcMain.handle("remote-folder-check", async (_event, path: string) => {
try {
const accessible = await remoteFolderService.isPathAccessible(path);
return { success: accessible };
} catch (error) {
return { success: false };
}
});
};
Loading