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:
nemmy 2026-06-07 13:13:33 -04:00 • committed by GitHub
parent 63faefec90
commit 9b3d39d055
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 9 deletions

View file

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

View file

@ -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: [

View file

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

View file

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

View 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