Fix recording sync (#291)
* delete audio/video/recording in remote * sync recordings on profile page * handle recording sync failed
This commit is contained in:
@@ -28,7 +28,7 @@ export class Client {
|
||||
});
|
||||
this.api.interceptors.request.use((config) => {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
config.headers['Accept-Language'] = locale;
|
||||
config.headers["Accept-Language"] = locale;
|
||||
|
||||
this.logger.debug(
|
||||
config.method.toUpperCase(),
|
||||
@@ -140,10 +140,18 @@ export class Client {
|
||||
return this.api.post("/api/mine/audios", decamelizeKeys(audio));
|
||||
}
|
||||
|
||||
deleteAudio(id: string) {
|
||||
return this.api.delete(`/api/mine/audios/${id}`);
|
||||
}
|
||||
|
||||
syncVideo(video: Partial<VideoType>) {
|
||||
return this.api.post("/api/mine/videos", decamelizeKeys(video));
|
||||
}
|
||||
|
||||
deleteVideo(id: string) {
|
||||
return this.api.delete(`/api/mine/videos/${id}`);
|
||||
}
|
||||
|
||||
syncTranscription(transcription: Partial<TranscriptionType>) {
|
||||
return this.api.post("/api/transcriptions", decamelizeKeys(transcription));
|
||||
}
|
||||
@@ -154,6 +162,10 @@ export class Client {
|
||||
return this.api.post("/api/mine/recordings", decamelizeKeys(recording));
|
||||
}
|
||||
|
||||
deleteRecording(id: string) {
|
||||
return this.api.delete(`/api/mine/recordings/${id}`);
|
||||
}
|
||||
|
||||
generateSpeechToken(): Promise<{ token: string; region: string }> {
|
||||
return this.api.post("/api/speech/tokens");
|
||||
}
|
||||
|
||||
@@ -444,5 +444,8 @@
|
||||
"generatingIpaFailed": "Generating IPA failed",
|
||||
"translating": "Translating",
|
||||
"translatedSuccessfully": "Translated successfully",
|
||||
"translationFailed": "Translation failed"
|
||||
"translationFailed": "Translation failed",
|
||||
"allRecordingsSynced": "All recordings synced",
|
||||
"syncingRecordings": "Syncing {{count}} recordings",
|
||||
"failedToSyncRecordings": "Syncing recordings failed"
|
||||
}
|
||||
|
||||
@@ -443,5 +443,8 @@
|
||||
"generatingIpaFailed": "音标生成失败",
|
||||
"translating": "正在翻译",
|
||||
"translatedSuccessfully": "翻译成功",
|
||||
"translationFailed": "翻译失败"
|
||||
"translationFailed": "翻译失败",
|
||||
"allRecordingsSynced": "所有录音已同步",
|
||||
"syncingRecordings": "{{count}} 条录音正在同步",
|
||||
"failedToSyncRecordings": "同步录音失败"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from "sequelize";
|
||||
import dayjs from "dayjs";
|
||||
import { t } from "i18next";
|
||||
import log from "electron-log/main";
|
||||
|
||||
const logger = log.scope("db/handlers/recordings-handler");
|
||||
|
||||
class RecordingsHandler {
|
||||
private async findAll(
|
||||
@@ -64,6 +67,36 @@ class RecordingsHandler {
|
||||
});
|
||||
}
|
||||
|
||||
private async syncAll(event: IpcMainEvent) {
|
||||
const recordings = await Recording.findAll({
|
||||
where: { syncedAt: null },
|
||||
});
|
||||
if (recordings.length == 0) return;
|
||||
|
||||
event.sender.send("on-notification", {
|
||||
type: "warning",
|
||||
message: t("syncingRecordings", { count: recordings.length }),
|
||||
});
|
||||
|
||||
try {
|
||||
recordings.forEach(async (recording) => {
|
||||
await recording.sync();
|
||||
});
|
||||
|
||||
event.sender.send("on-notification", {
|
||||
type: "info",
|
||||
message: t("allRecordingsSynced"),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error("failed to sync recordings", err.message);
|
||||
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: t("failedToSyncRecordings"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async create(
|
||||
event: IpcMainEvent,
|
||||
options: Attributes<Recording> & {
|
||||
@@ -293,7 +326,7 @@ class RecordingsHandler {
|
||||
private async groupBySegment(
|
||||
event: IpcMainEvent,
|
||||
targetId: string,
|
||||
targetType: string,
|
||||
targetType: string
|
||||
) {
|
||||
return Recording.findAll({
|
||||
where: {
|
||||
@@ -328,6 +361,7 @@ class RecordingsHandler {
|
||||
register() {
|
||||
ipcMain.handle("recordings-find-all", this.findAll);
|
||||
ipcMain.handle("recordings-find-one", this.findOne);
|
||||
ipcMain.handle("recordings-sync-all", this.syncAll);
|
||||
ipcMain.handle("recordings-create", this.create);
|
||||
ipcMain.handle("recordings-destroy", this.destroy);
|
||||
ipcMain.handle("recordings-upload", this.upload);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "sequelize-typescript";
|
||||
import { Recording, Speech, Transcription, Video } from "@main/db/models";
|
||||
import settings from "@main/settings";
|
||||
import { AudioFormats, VideoFormats , WEB_API_URL } from "@/constants";
|
||||
import { AudioFormats, VideoFormats, WEB_API_URL } from "@/constants";
|
||||
import { hashFile } from "@/utils";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
@@ -170,7 +170,7 @@ export class Audio extends Model<Audio> {
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("api/client"),
|
||||
logger: log.scope("audio/sync"),
|
||||
});
|
||||
|
||||
return webApi.syncAudio(this.toJSON()).then(() => {
|
||||
@@ -231,6 +231,16 @@ export class Audio extends Model<Audio> {
|
||||
targetType: "Audio",
|
||||
},
|
||||
});
|
||||
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("audio/cleanupFile"),
|
||||
});
|
||||
|
||||
webApi.deleteAudio(audio.id).catch((err) => {
|
||||
logger.error("deleteAudio failed:", err.message);
|
||||
});
|
||||
}
|
||||
|
||||
static async buildFromLocalFile(
|
||||
|
||||
@@ -139,7 +139,7 @@ export class Recording extends Model<Recording> {
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("api/client"),
|
||||
logger: log.scope("recording/sync"),
|
||||
});
|
||||
|
||||
return webApi.syncRecording(this.toJSON()).then(() => {
|
||||
@@ -160,7 +160,7 @@ export class Recording extends Model<Recording> {
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("api/client"),
|
||||
logger: log.scope("recording/assess"),
|
||||
});
|
||||
|
||||
const { token, region } = await webApi.generateSpeechToken();
|
||||
@@ -274,6 +274,14 @@ export class Recording extends Model<Recording> {
|
||||
@AfterDestroy
|
||||
static cleanupFile(recording: Recording) {
|
||||
fs.remove(recording.filePath);
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("recording/cleanupFile"),
|
||||
});
|
||||
webApi.deleteRecording(recording.id).catch((err) => {
|
||||
logger.error("deleteRecording failed:", err.message);
|
||||
});
|
||||
}
|
||||
|
||||
static async createFromBlob(
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "sequelize-typescript";
|
||||
import { Audio, Recording, Speech, Transcription } from "@main/db/models";
|
||||
import settings from "@main/settings";
|
||||
import { AudioFormats, VideoFormats , WEB_API_URL } from "@/constants";
|
||||
import { AudioFormats, VideoFormats, WEB_API_URL } from "@/constants";
|
||||
import { hashFile } from "@/utils";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
@@ -192,7 +192,7 @@ export class Video extends Model<Video> {
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("api/client"),
|
||||
logger: log.scope("video/sync"),
|
||||
});
|
||||
|
||||
return webApi.syncVideo(this.toJSON()).then(() => {
|
||||
@@ -254,6 +254,16 @@ export class Video extends Model<Video> {
|
||||
targetType: "Video",
|
||||
},
|
||||
});
|
||||
|
||||
const webApi = new Client({
|
||||
baseUrl: process.env.WEB_API_URL || WEB_API_URL,
|
||||
accessToken: settings.getSync("user.accessToken") as string,
|
||||
logger: log.scope("video/cleanupFile"),
|
||||
});
|
||||
|
||||
webApi.deleteVideo(video.id).catch((err) => {
|
||||
logger.error("deleteAudio failed:", err.message);
|
||||
});
|
||||
}
|
||||
|
||||
static async buildFromLocalFile(
|
||||
|
||||
@@ -236,6 +236,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
findOne: (params: object) => {
|
||||
return ipcRenderer.invoke("recordings-find-one", params);
|
||||
},
|
||||
syncAll: () => {
|
||||
return ipcRenderer.invoke("recordings-sync-all");
|
||||
},
|
||||
create: (params: object) => {
|
||||
return ipcRenderer.invoke("recordings-create", params);
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState, useContext } from "react";
|
||||
import {
|
||||
RecordingStats,
|
||||
RecordingCalendar,
|
||||
RecordingActivities,
|
||||
} from "@renderer/components";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { ChevronLeftIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -13,11 +14,16 @@ import { t } from "i18next";
|
||||
export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const [range, setRange] = useState<[string, string]>([
|
||||
dayjs().subtract(7, "day").format(),
|
||||
dayjs().format(),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.recordings.syncAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full px-4 py-6 lg:px-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
|
||||
29
enjoy/src/types/enjoy-app.d.ts
vendored
29
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -51,7 +51,7 @@ type EnjoyAppType = {
|
||||
navigatable?: boolean;
|
||||
}
|
||||
) => Promise<void>;
|
||||
show: (bounds: object) => Promise<void>;
|
||||
show: (bounds: any) => Promise<void>;
|
||||
hide: () => Promise<void>;
|
||||
remove: () => Promise<void>;
|
||||
scrape: (url: string) => Promise<void>;
|
||||
@@ -112,26 +112,27 @@ type EnjoyAppType = {
|
||||
removeListeners: () => Promise<void>;
|
||||
};
|
||||
audios: {
|
||||
findAll: (params: object) => Promise<AudioType[]>;
|
||||
findOne: (params: object) => Promise<AudioType>;
|
||||
create: (uri: string, params?: object) => Promise<AudioType>;
|
||||
update: (id: string, params: object) => Promise<AudioType | undefined>;
|
||||
findAll: (params: any) => Promise<AudioType[]>;
|
||||
findOne: (params: any) => Promise<AudioType>;
|
||||
create: (uri: string, params?: any) => Promise<AudioType>;
|
||||
update: (id: string, params: any) => Promise<AudioType | undefined>;
|
||||
destroy: (id: string) => Promise<undefined>;
|
||||
upload: (id: string) => Promise<void>;
|
||||
};
|
||||
videos: {
|
||||
findAll: (params: object) => Promise<VideoType[]>;
|
||||
findOne: (params: object) => Promise<VideoType>;
|
||||
findAll: (params: any) => Promise<VideoType[]>;
|
||||
findOne: (params: any) => Promise<VideoType>;
|
||||
create: (uri: string, params?: any) => Promise<VideoType>;
|
||||
update: (id: string, params: any) => Promise<VideoType | undefined>;
|
||||
destroy: (id: string) => Promise<undefined>;
|
||||
upload: (id: string) => Promise<void>;
|
||||
};
|
||||
recordings: {
|
||||
findAll: (where: object) => Promise<RecordingType[]>;
|
||||
findOne: (where: object) => Promise<RecordingType>;
|
||||
create: (params: object) => Promise<RecordingType>;
|
||||
update: (id: string, params: object) => Promise<RecordingType | undefined>;
|
||||
findAll: (where: any) => Promise<RecordingType[]>;
|
||||
findOne: (where: any) => Promise<RecordingType>;
|
||||
syncAll: () => Promise<void>;
|
||||
create: (params: any) => Promise<RecordingType>;
|
||||
update: (id: string, params: any) => Promise<RecordingType | undefined>;
|
||||
destroy: (id: string) => Promise<void>;
|
||||
upload: (id: string) => Promise<void>;
|
||||
assess: (id: string) => Promise<void>;
|
||||
@@ -165,7 +166,7 @@ type EnjoyAppType = {
|
||||
findAll: (params: any) => Promise<ConversationType[]>;
|
||||
findOne: (params: any) => Promise<ConversationType>;
|
||||
create: (params: any) => Promise<ConversationType>;
|
||||
update: (id: string, params: object) => Promise<ConversationType>;
|
||||
update: (id: string, params: any) => Promise<ConversationType>;
|
||||
destroy: (id: string) => Promise<void>;
|
||||
ask: (
|
||||
id: string,
|
||||
@@ -181,8 +182,8 @@ type EnjoyAppType = {
|
||||
) => Promise<MessageType[]>;
|
||||
};
|
||||
messages: {
|
||||
findAll: (params: object) => Promise<MessageType[]>;
|
||||
findOne: (params: object) => Promise<MessageType>;
|
||||
findAll: (params: any) => Promise<MessageType[]>;
|
||||
findOne: (params: any) => Promise<MessageType>;
|
||||
createInBatch: (messages: Partial<MessageType>[]) => Promise<void>;
|
||||
destroy: (id: string) => Promise<void>;
|
||||
createSpeech: (id: string, configuration?: any) => Promise<SpeechType>;
|
||||
|
||||
Reference in New Issue
Block a user