Fix recording sync (#291)

* delete audio/video/recording in remote

* sync recordings on profile page

* handle recording sync failed
This commit is contained in:
an-lee
2024-02-09 18:24:36 +08:00
committed by GitHub
parent 27c342cabb
commit 338ef82a1e
10 changed files with 115 additions and 25 deletions

View File

@@ -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");
}

View File

@@ -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"
}

View File

@@ -443,5 +443,8 @@
"generatingIpaFailed": "音标生成失败",
"translating": "正在翻译",
"translatedSuccessfully": "翻译成功",
"translationFailed": "翻译失败"
"translationFailed": "翻译失败",
"allRecordingsSynced": "所有录音已同步",
"syncingRecordings": "{{count}} 条录音正在同步",
"failedToSyncRecordings": "同步录音失败"
}

View File

@@ -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);

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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);
},

View File

@@ -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">

View File

@@ -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>;