update clipupload to support parsing from existing metadata (#1148)
* update clipupload to support parsing from existing metadata * fuck you greptile * add self as author
This commit is contained in:
parent
63faefec90
commit
9b3d39d055
5 changed files with 91 additions and 9 deletions
|
|
@ -9,7 +9,7 @@ import { Flex } from "@components/Flex";
|
|||
import { Heading } from "@components/Heading";
|
||||
import { Paragraph } from "@components/Paragraph";
|
||||
import type { RenderModalProps } from "@vencord/discord-types";
|
||||
import { Modal, openModal, showToast, Toasts, useEffect, useState } from "@webpack/common";
|
||||
import { Checkbox, Modal, openModal, showToast, Toasts, useEffect, useState } from "@webpack/common";
|
||||
|
||||
import { ApplicationField, BooleanField, DateTimeField, getDateTimeLocalValue, ParticipantField, TextField } from "./fields";
|
||||
import { abortActiveClipUploads, type ClipMetadata, getClipCreatedAt, getClipTitleFromName, getDefaultClipTitle, getDefaultFileName, getErrorMessage, getParticipantIds, getString, isValidDate, pickClipFile, uploadClipFile } from "./upload";
|
||||
|
|
@ -37,6 +37,7 @@ function UploadClipFileModal({ modalProps, channelId, clip }: { modalProps: Rend
|
|||
const [message, setMessage] = useState("");
|
||||
const [applicationId, setApplicationId] = useState(getString(clip?.applicationId) ?? "");
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [parseMetadata, setParseMetadata] = useState(false);
|
||||
|
||||
const canUpload = Boolean(file && fileName.trim() && title.trim() && isValidDate(createdAt)) && !uploading;
|
||||
const notice = createdAt && !isValidDate(createdAt)
|
||||
|
|
@ -46,18 +47,28 @@ function UploadClipFileModal({ modalProps, channelId, clip }: { modalProps: Rend
|
|||
useEffect(() => abortActiveClipUploads, []);
|
||||
|
||||
async function chooseClipFile() {
|
||||
let picked: File | null;
|
||||
let result;
|
||||
try {
|
||||
picked = await pickClipFile();
|
||||
if (!picked) return;
|
||||
result = await pickClipFile(parseMetadata);
|
||||
if (!result) return;
|
||||
} catch (error) {
|
||||
showToast(getErrorMessage(error), Toasts.Type.FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
const { file: picked, metadata } = result;
|
||||
|
||||
setFile(picked);
|
||||
setFileName(name => name === defaultFileName ? picked.name : name);
|
||||
setTitle(currentTitle => currentTitle || getClipTitleFromName(picked.name));
|
||||
|
||||
const appName = metadata?.applicationName;
|
||||
const appUsers = metadata?.users;
|
||||
const appId = metadata?.applicationId;
|
||||
|
||||
setTitle(currentTitle => appName || currentTitle || getClipTitleFromName(picked.name));
|
||||
|
||||
if (appUsers?.length) setParticipants(current => current.length ? current : appUsers);
|
||||
if (appId) setApplicationId(current => current || appId);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
|
|
@ -117,6 +128,14 @@ function UploadClipFileModal({ modalProps, channelId, clip }: { modalProps: Rend
|
|||
</Button>
|
||||
<Paragraph>{file?.name ?? "No file selected"}</Paragraph>
|
||||
</Flex>
|
||||
<Checkbox
|
||||
value={parseMetadata}
|
||||
onChange={(_event, checked) => setParseMetadata(checked)}
|
||||
disabled={uploading}
|
||||
type="row"
|
||||
>
|
||||
Parse clip metadata from file
|
||||
</Checkbox>
|
||||
</section>
|
||||
|
||||
<TextField title="File name" value={fileName} onChange={setFileName} placeholder="my_clip.mp4" disabled={uploading} />
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const ctxMenuPatch: NavContextMenuPatchCallback = (children, props) => {
|
|||
export default definePlugin({
|
||||
name: "ClipUpload",
|
||||
description: "Adds a button to upload a local video file as a Discord clip.",
|
||||
authors: [EquicordDevs.qdnx, EquicordDevs.BachLe2000],
|
||||
authors: [EquicordDevs.qdnx, EquicordDevs.BachLe2000, EquicordDevs.pandaptable],
|
||||
tags: ["Media", "Utility"],
|
||||
|
||||
patches: [
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ interface TempEntry {
|
|||
tmpPath: string;
|
||||
}
|
||||
|
||||
interface RawClipAttachment {
|
||||
id?: string;
|
||||
applicationId?: string;
|
||||
applicationName?: string;
|
||||
users?: string[];
|
||||
version?: number;
|
||||
}
|
||||
|
||||
const pendingTokens = new Map<string, string>();
|
||||
const tempEntries = new Map<string, TempEntry>();
|
||||
const CLIP_UPLOAD_DIR = join(DATA_DIR, "clipUpload");
|
||||
|
|
@ -25,10 +33,38 @@ const MIME_TYPES: Record<string, string> = {
|
|||
".m4v": "video/mp4",
|
||||
};
|
||||
|
||||
const CLIP_FOOTER = Buffer.from([
|
||||
0x75, 0x75, 0x69, 0x64,
|
||||
0xA1, 0xC8, 0x52, 0x99, 0x33, 0x46, 0x4D, 0xB8, 0x88, 0xF0, 0x83, 0xF5, 0x7A, 0x75, 0xA5, 0xEF,
|
||||
]);
|
||||
const CLIP_FOOTER_SIZE = CLIP_FOOTER.length;
|
||||
|
||||
function getMimeType(filePath: string): string {
|
||||
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
function stripClipFooter(data: Buffer): Buffer {
|
||||
const idx = data.indexOf(CLIP_FOOTER);
|
||||
return idx === -1 ? data : data.subarray(0, idx);
|
||||
}
|
||||
|
||||
async function parseClipMetadata(filePath: string): Promise<RawClipAttachment[] | null> {
|
||||
try {
|
||||
const buf = await readFile(filePath);
|
||||
if (buf.length <= CLIP_FOOTER_SIZE) return null;
|
||||
|
||||
const footerIdx = buf.indexOf(CLIP_FOOTER);
|
||||
if (footerIdx === -1) return null;
|
||||
|
||||
const jsonStr = buf.subarray(footerIdx + CLIP_FOOTER_SIZE).toString("utf-8");
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
if (parsed && typeof parsed === "object") return Array.isArray(parsed) ? parsed : [parsed];
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function chooseVideoFile(_: IpcMainInvokeEvent): Promise<{ token: string; name: string; type: string; } | null> {
|
||||
try {
|
||||
const { filePaths, canceled } = await dialog.showOpenDialog({
|
||||
|
|
@ -63,7 +99,7 @@ export async function createTempVideoFile(_: IpcMainInvokeEvent, token: string):
|
|||
if (!ensureSafePath(tmpDir, basename(originalPath))) return null;
|
||||
|
||||
await mkdir(tmpDir, { recursive: true });
|
||||
await writeFile(tmpPath, await readFile(originalPath));
|
||||
await writeFile(tmpPath, stripClipFooter(await readFile(originalPath)));
|
||||
|
||||
const tmpToken = randomUUID();
|
||||
tempEntries.set(tmpToken, { tmpDir, tmpPath });
|
||||
|
|
@ -97,3 +133,10 @@ export async function deleteTempVideoFile(_: IpcMainInvokeEvent, token: string):
|
|||
await rm(entry.tmpDir, { force: true, recursive: true });
|
||||
} catch { }
|
||||
}
|
||||
|
||||
export async function parseClipFileMetadata(_: IpcMainInvokeEvent, token: string): Promise<RawClipAttachment[] | null> {
|
||||
const originalPath = pendingTokens.get(token);
|
||||
if (!originalPath) return null;
|
||||
|
||||
return parseClipMetadata(originalPath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,11 +121,27 @@ async function readStampedVideoFile(token: string, name: string, type: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function pickClipFile() {
|
||||
export interface ParsedClipMetadata {
|
||||
id?: string;
|
||||
applicationId?: string;
|
||||
applicationName?: string;
|
||||
users?: string[];
|
||||
version?: number;
|
||||
}
|
||||
|
||||
export async function pickClipFile(parseFileMetadata: boolean) {
|
||||
const picked = await Native.chooseVideoFile();
|
||||
if (!picked) return null;
|
||||
|
||||
return readStampedVideoFile(picked.token, picked.name, picked.type);
|
||||
let metadata: ParsedClipMetadata | null = null;
|
||||
if (parseFileMetadata) {
|
||||
const result = await Native.parseClipFileMetadata(picked.token);
|
||||
metadata = result?.[0] ?? null;
|
||||
}
|
||||
|
||||
const file = await readStampedVideoFile(picked.token, picked.name, picked.type);
|
||||
|
||||
return { file, metadata };
|
||||
}
|
||||
|
||||
function isCompatibleClipFile(file: File) {
|
||||
|
|
|
|||
|
|
@ -1341,6 +1341,10 @@ export const EquicordDevs = Object.freeze({
|
|||
name: "BachLe2000",
|
||||
id: 624091967625625610n
|
||||
},
|
||||
pandaptable: {
|
||||
name: "pandaptable",
|
||||
id: 97153209843335168n
|
||||
},
|
||||
} satisfies Record<string, Dev>);
|
||||
|
||||
// iife so #__PURE__ works correctly
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue