mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-18 03:17:29 +08:00
✨ feat: 添加统计服务
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
"font-list": "^1.5.1",
|
"font-list": "^1.5.1",
|
||||||
"netease-cloud-music-api-alger": "^4.26.1",
|
"netease-cloud-music-api-alger": "^4.26.1",
|
||||||
"node-id3": "^0.2.9",
|
"node-id3": "^0.2.9",
|
||||||
|
"node-machine-id": "^1.1.12",
|
||||||
"vue-i18n": "9"
|
"vue-i18n": "9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { initializeConfig } from './modules/config';
|
|||||||
import { initializeFileManager } from './modules/fileManager';
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
import { initializeFonts } from './modules/fonts';
|
import { initializeFonts } from './modules/fonts';
|
||||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||||
|
import { initializeStats, setupStatsHandlers } from './modules/statsService';
|
||||||
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||||
import { setupUpdateHandlers } from './modules/update';
|
import { setupUpdateHandlers } from './modules/update';
|
||||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
@@ -50,6 +51,12 @@ function initialize() {
|
|||||||
// 初始化托盘
|
// 初始化托盘
|
||||||
initializeTray(iconPath, mainWindow);
|
initializeTray(iconPath, mainWindow);
|
||||||
|
|
||||||
|
// 初始化统计服务
|
||||||
|
initializeStats();
|
||||||
|
|
||||||
|
// 设置统计相关的IPC处理程序
|
||||||
|
setupStatsHandlers(ipcMain);
|
||||||
|
|
||||||
// 启动音乐API
|
// 启动音乐API
|
||||||
startMusicApi();
|
startMusicApi();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
import { machineIdSync } from 'node-machine-id';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备唯一标识符
|
||||||
|
* 优先使用存储的ID,如果没有则获取机器ID并存储
|
||||||
|
*/
|
||||||
|
export function getDeviceId(): string {
|
||||||
|
let deviceId = store.get('deviceId') as string | undefined;
|
||||||
|
|
||||||
|
if (!deviceId) {
|
||||||
|
try {
|
||||||
|
// 使用node-machine-id获取设备唯一标识
|
||||||
|
deviceId = machineIdSync(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取机器ID失败:', error);
|
||||||
|
// 如果获取失败,使用主机名和MAC地址组合作为备选方案
|
||||||
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
let macAddress = '';
|
||||||
|
|
||||||
|
// 尝试获取第一个非内部网络接口的MAC地址
|
||||||
|
Object.values(networkInterfaces).forEach((interfaces) => {
|
||||||
|
if (interfaces) {
|
||||||
|
interfaces.forEach((iface) => {
|
||||||
|
if (!iface.internal && !macAddress && iface.mac !== '00:00:00:00:00:00') {
|
||||||
|
macAddress = iface.mac;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceId = `${os.hostname()}-${macAddress}`.replace(/:/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储设备ID
|
||||||
|
if (deviceId) {
|
||||||
|
store.set('deviceId', deviceId);
|
||||||
|
} else {
|
||||||
|
// 如果所有方法都失败,使用随机ID
|
||||||
|
deviceId = Math.random().toString(36).substring(2, 15);
|
||||||
|
store.set('deviceId', deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取系统信息
|
||||||
|
*/
|
||||||
|
export function getSystemInfo() {
|
||||||
|
return {
|
||||||
|
osType: os.type(),
|
||||||
|
osVersion: os.release(),
|
||||||
|
osArch: os.arch(),
|
||||||
|
platform: process.platform,
|
||||||
|
appVersion: app.getVersion()
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { app } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
import { getDeviceId, getSystemInfo } from './deviceInfo';
|
||||||
|
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
// 统计服务配置
|
||||||
|
const STATS_API_URL = 'http://donate.alger.fun/state/api/stats';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录应用安装/启动
|
||||||
|
*/
|
||||||
|
export async function recordInstallation(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const deviceId = getDeviceId();
|
||||||
|
const systemInfo = getSystemInfo();
|
||||||
|
|
||||||
|
// 发送请求到统计服务器
|
||||||
|
await axios.post(`${STATS_API_URL}/installation`, {
|
||||||
|
deviceId,
|
||||||
|
osType: systemInfo.osType,
|
||||||
|
osVersion: systemInfo.osVersion,
|
||||||
|
appVersion: systemInfo.appVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('应用启动统计已记录');
|
||||||
|
|
||||||
|
// 记录最后一次启动时间
|
||||||
|
store.set('lastStartTime', new Date().toISOString());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录应用启动统计失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 IPC 处理程序以接收渲染进程的统计请求
|
||||||
|
* @param ipcMain Electron IPC主对象
|
||||||
|
*/
|
||||||
|
export function setupStatsHandlers(ipcMain: Electron.IpcMain): void {
|
||||||
|
// 处理页面访问统计
|
||||||
|
ipcMain.handle('record-visit', async (_, page: string, userId?: string) => {
|
||||||
|
try {
|
||||||
|
const deviceId = getDeviceId();
|
||||||
|
|
||||||
|
await axios.post(`${STATS_API_URL}/visit`, {
|
||||||
|
deviceId,
|
||||||
|
userId,
|
||||||
|
page
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录页面访问统计失败:', error);
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理播放统计
|
||||||
|
ipcMain.handle(
|
||||||
|
'record-play',
|
||||||
|
async (
|
||||||
|
_,
|
||||||
|
songData: {
|
||||||
|
userId: string | null;
|
||||||
|
songId: string | number;
|
||||||
|
songName: string;
|
||||||
|
artistName: string;
|
||||||
|
duration?: number;
|
||||||
|
completedPlay?: boolean;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { songId, songName, artistName, duration = 0, completedPlay = false } = songData;
|
||||||
|
const deviceId = getDeviceId();
|
||||||
|
|
||||||
|
await axios.post(`${STATS_API_URL}/play`, {
|
||||||
|
deviceId,
|
||||||
|
userId: songData.userId,
|
||||||
|
songId: songId.toString(),
|
||||||
|
songName,
|
||||||
|
artistName,
|
||||||
|
duration,
|
||||||
|
completedPlay
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录播放统计失败:', error);
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理获取统计摘要
|
||||||
|
ipcMain.handle('get-stats-summary', async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${STATS_API_URL}/summary`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计摘要失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用启动时初始化统计服务
|
||||||
|
*/
|
||||||
|
export function initializeStats(): void {
|
||||||
|
// 记录应用启动统计
|
||||||
|
recordInstallation().catch((error) => {
|
||||||
|
console.error('初始化统计服务失败:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册应用退出时的回调
|
||||||
|
app.on('will-quit', () => {
|
||||||
|
// 可以在这里添加应用退出时的统计逻辑
|
||||||
|
console.log('应用退出');
|
||||||
|
});
|
||||||
|
}
|
||||||
Vendored
+34
-23
@@ -1,31 +1,42 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload';
|
import { ElectronAPI } from '@electron-toolkit/preload';
|
||||||
|
|
||||||
|
interface API {
|
||||||
|
minimize: () => void;
|
||||||
|
maximize: () => void;
|
||||||
|
close: () => void;
|
||||||
|
dragStart: (data: any) => void;
|
||||||
|
miniTray: () => void;
|
||||||
|
miniWindow: () => void;
|
||||||
|
restore: () => void;
|
||||||
|
restart: () => void;
|
||||||
|
resizeWindow: (width: number, height: number) => void;
|
||||||
|
resizeMiniWindow: (showPlaylist: boolean) => void;
|
||||||
|
openLyric: () => void;
|
||||||
|
sendLyric: (data: any) => void;
|
||||||
|
sendSong: (data: any) => void;
|
||||||
|
unblockMusic: (id: number, data: any) => Promise<any>;
|
||||||
|
onLyricWindowClosed: (callback: () => void) => void;
|
||||||
|
startDownload: (url: string) => void;
|
||||||
|
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
||||||
|
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
|
||||||
|
onLanguageChanged: (callback: (locale: string) => void) => void;
|
||||||
|
removeDownloadListeners: () => void;
|
||||||
|
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义IPC渲染进程通信接口
|
||||||
|
interface IpcRenderer {
|
||||||
|
send: (channel: string, ...args: any[]) => void;
|
||||||
|
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||||
|
on: (channel: string, listener: (...args: any[]) => void) => () => void;
|
||||||
|
removeAllListeners: (channel: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI;
|
electron: ElectronAPI;
|
||||||
api: {
|
api: API;
|
||||||
sendLyric: (data: string) => void;
|
ipcRenderer: IpcRenderer;
|
||||||
openLyric: () => void;
|
|
||||||
minimize: () => void;
|
|
||||||
maximize: () => void;
|
|
||||||
close: () => void;
|
|
||||||
dragStart: (data: string) => void;
|
|
||||||
miniTray: () => void;
|
|
||||||
miniWindow: () => void;
|
|
||||||
restore: () => void;
|
|
||||||
restart: () => void;
|
|
||||||
resizeWindow: (width: number, height: number) => void;
|
|
||||||
resizeMiniWindow: (showPlaylist: boolean) => void;
|
|
||||||
unblockMusic: (id: number, data: any) => Promise<any>;
|
|
||||||
onLyricWindowClosed: (callback: () => void) => void;
|
|
||||||
startDownload: (url: string) => void;
|
|
||||||
onDownloadProgress: (callback: (progress: number, status: string) => void) => void;
|
|
||||||
onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;
|
|
||||||
removeDownloadListeners: () => void;
|
|
||||||
onLanguageChanged: (callback: (locale: string) => void) => void;
|
|
||||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
|
||||||
sendSong: (data: any) => void;
|
|
||||||
};
|
|
||||||
$message: any;
|
$message: any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-1
@@ -47,7 +47,11 @@ const api = {
|
|||||||
'get-system-fonts',
|
'get-system-fonts',
|
||||||
'get-cached-lyric',
|
'get-cached-lyric',
|
||||||
'cache-lyric',
|
'cache-lyric',
|
||||||
'clear-lyric-cache'
|
'clear-lyric-cache',
|
||||||
|
// 统计相关
|
||||||
|
'record-visit',
|
||||||
|
'record-play',
|
||||||
|
'get-stats-summary'
|
||||||
];
|
];
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
return ipcRenderer.invoke(channel, ...args);
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
@@ -56,6 +60,29 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建带类型的ipcRenderer对象,暴露给渲染进程
|
||||||
|
const ipc = {
|
||||||
|
// 发送消息到主进程(无返回值)
|
||||||
|
send: (channel: string, ...args: any[]) => {
|
||||||
|
ipcRenderer.send(channel, ...args);
|
||||||
|
},
|
||||||
|
// 调用主进程方法(有返回值)
|
||||||
|
invoke: (channel: string, ...args: any[]) => {
|
||||||
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
|
},
|
||||||
|
// 监听主进程消息
|
||||||
|
on: (channel: string, listener: (...args: any[]) => void) => {
|
||||||
|
ipcRenderer.on(channel, (_, ...args) => listener(...args));
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.removeListener(channel, listener);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// 移除所有监听器
|
||||||
|
removeAllListeners: (channel: string) => {
|
||||||
|
ipcRenderer.removeAllListeners(channel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
// renderer only if context isolation is enabled, otherwise
|
// renderer only if context isolation is enabled, otherwise
|
||||||
// just add to the DOM global.
|
// just add to the DOM global.
|
||||||
@@ -63,6 +90,7 @@ if (process.contextIsolated) {
|
|||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
contextBridge.exposeInMainWorld('api', api);
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
|
contextBridge.exposeInMainWorld('ipcRenderer', ipc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@@ -71,4 +99,6 @@ if (process.contextIsolated) {
|
|||||||
window.electron = electronAPI;
|
window.electron = electronAPI;
|
||||||
// @ts-ignore (define in dts)
|
// @ts-ignore (define in dts)
|
||||||
window.api = api;
|
window.api = api;
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.ipcRenderer = ipc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ export interface Donor {
|
|||||||
* 获取捐赠列表
|
* 获取捐赠列表
|
||||||
*/
|
*/
|
||||||
export const getDonationList = async (): Promise<Donor[]> => {
|
export const getDonationList = async (): Promise<Donor[]> => {
|
||||||
const { data } = await axios.get('http://110.42.251.190:8766/api/donations');
|
const { data } = await axios.get('http://donate.alger.fun/api/donations');
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { isElectron } from '@/utils';
|
||||||
|
|
||||||
|
import { useUserStore } from '../store/modules/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户ID
|
||||||
|
* @returns 用户ID或null
|
||||||
|
*/
|
||||||
|
function getUserId(): string | null {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
return userStore.user?.userId?.toString() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录页面访问
|
||||||
|
* @param page 页面名称或路径
|
||||||
|
*/
|
||||||
|
export async function recordVisit(page: string): Promise<void> {
|
||||||
|
if (!isElectron) return;
|
||||||
|
try {
|
||||||
|
const userId = getUserId();
|
||||||
|
await window.api.invoke('record-visit', page, userId);
|
||||||
|
console.log(`页面访问已记录: ${page}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录页面访问失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录歌曲播放
|
||||||
|
* @param songId 歌曲ID
|
||||||
|
* @param songName 歌曲名称
|
||||||
|
* @param artistName 艺术家名称
|
||||||
|
* @param duration 时长(秒)
|
||||||
|
* @param completedPlay 是否完整播放
|
||||||
|
*/
|
||||||
|
export async function recordPlay(
|
||||||
|
songId: string | number,
|
||||||
|
songName: string,
|
||||||
|
artistName: string,
|
||||||
|
duration: number = 0,
|
||||||
|
completedPlay: boolean = false
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isElectron) return;
|
||||||
|
try {
|
||||||
|
const userId = getUserId();
|
||||||
|
|
||||||
|
await window.api.invoke('record-play', {
|
||||||
|
userId,
|
||||||
|
songId,
|
||||||
|
songName,
|
||||||
|
artistName,
|
||||||
|
duration,
|
||||||
|
completedPlay
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`歌曲播放已记录: ${songName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录歌曲播放失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统计摘要
|
||||||
|
* @returns 统计数据摘要
|
||||||
|
*/
|
||||||
|
export async function getStatsSummary(): Promise<any> {
|
||||||
|
if (!isElectron) return null;
|
||||||
|
try {
|
||||||
|
return await window.api.invoke('get-stats-summary');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计摘要失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// musicHistoryHooks
|
// musicHistoryHooks
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { recordPlay } from '@/api/stats';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
|
|
||||||
export const useMusicHistory = () => {
|
export const useMusicHistory = () => {
|
||||||
@@ -14,6 +15,25 @@ export const useMusicHistory = () => {
|
|||||||
} else {
|
} else {
|
||||||
musicHistory.value.unshift({ ...music, count: 1 });
|
musicHistory.value.unshift({ ...music, count: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录播放统计
|
||||||
|
if (music?.id && music?.name) {
|
||||||
|
// 获取艺术家名称
|
||||||
|
let artistName = '未知艺术家';
|
||||||
|
|
||||||
|
if (music.ar) {
|
||||||
|
artistName = music.ar.map((artist) => artist.name).join('/');
|
||||||
|
} else if (music.song?.artists && music.song.artists.length > 0) {
|
||||||
|
artistName = music.song.artists.map((artist) => artist.name).join('/');
|
||||||
|
} else if (music.artists) {
|
||||||
|
artistName = music.artists.map((artist) => artist.name).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送播放统计
|
||||||
|
recordPlay(music.id, music.name, artistName).catch((error) =>
|
||||||
|
console.error('记录播放统计失败:', error)
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const delMusic = (music: SongResult) => {
|
const delMusic = (music: SongResult) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
|
import { recordVisit } from '@/api/stats';
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import AppLayout from '@/layout/AppLayout.vue';
|
||||||
import MiniLayout from '@/layout/MiniLayout.vue';
|
import MiniLayout from '@/layout/MiniLayout.vue';
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
@@ -80,4 +81,13 @@ router.beforeEach((to, _, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加全局后置钩子,记录页面访问
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const pageName = to.name?.toString() || to.path;
|
||||||
|
// 使用setTimeout避免阻塞路由导航
|
||||||
|
setTimeout(() => {
|
||||||
|
recordVisit(pageName).catch((error) => console.error('记录页面访问失败:', error));
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user