diff --git a/src/equicordplugins/clipUpload.desktop/UploadClipFileModal.tsx b/src/equicordplugins/clipUpload.desktop/UploadClipFileModal.tsx
index 961aad2ae..640199871 100644
--- a/src/equicordplugins/clipUpload.desktop/UploadClipFileModal.tsx
+++ b/src/equicordplugins/clipUpload.desktop/UploadClipFileModal.tsx
@@ -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
{file?.name ?? "No file selected"}
+ setParseMetadata(checked)}
+ disabled={uploading}
+ type="row"
+ >
+ Parse clip metadata from file
+
diff --git a/src/equicordplugins/clipUpload.desktop/index.tsx b/src/equicordplugins/clipUpload.desktop/index.tsx
index d95a675dd..a8e03ce1c 100644
--- a/src/equicordplugins/clipUpload.desktop/index.tsx
+++ b/src/equicordplugins/clipUpload.desktop/index.tsx
@@ -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: [
diff --git a/src/equicordplugins/clipUpload.desktop/native.ts b/src/equicordplugins/clipUpload.desktop/native.ts
index 62fa7aea3..152180922 100644
--- a/src/equicordplugins/clipUpload.desktop/native.ts
+++ b/src/equicordplugins/clipUpload.desktop/native.ts
@@ -16,6 +16,14 @@ interface TempEntry {
tmpPath: string;
}
+interface RawClipAttachment {
+ id?: string;
+ applicationId?: string;
+ applicationName?: string;
+ users?: string[];
+ version?: number;
+}
+
const pendingTokens = new Map();
const tempEntries = new Map();
const CLIP_UPLOAD_DIR = join(DATA_DIR, "clipUpload");
@@ -25,10 +33,38 @@ const MIME_TYPES: Record = {
".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 {
+ 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 {
+ const originalPath = pendingTokens.get(token);
+ if (!originalPath) return null;
+
+ return parseClipMetadata(originalPath);
+}
diff --git a/src/equicordplugins/clipUpload.desktop/upload.ts b/src/equicordplugins/clipUpload.desktop/upload.ts
index a28bd80b6..6124a7c9d 100644
--- a/src/equicordplugins/clipUpload.desktop/upload.ts
+++ b/src/equicordplugins/clipUpload.desktop/upload.ts
@@ -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) {
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index a4bce2565..6ba611d34 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -1341,6 +1341,10 @@ export const EquicordDevs = Object.freeze({
name: "BachLe2000",
id: 624091967625625610n
},
+ pandaptable: {
+ name: "pandaptable",
+ id: 97153209843335168n
+ },
} satisfies Record);
// iife so #__PURE__ works correctly