Add Vimeo video import with embeds and admin re-run

Import public videos from jmartgraphix as Video (and 3D Animation
when relevant), with player embeds, thumbnails, and dashboard UI.
This commit is contained in:
2026-07-24 12:13:23 -04:00
parent 6ff7fde867
commit b924ca8a62
7 changed files with 602 additions and 2 deletions
+13
View File
@@ -264,6 +264,19 @@ export const api = {
importFlickrStatus: () =>
request<ArtStationImportStatus>("/api/v1/admin/import/flickr"),
importVimeo: (body: { username?: string; dryRun?: boolean; publish?: boolean }) =>
request<{
ok: boolean;
message: string;
status: ArtStationImportStatus;
}>("/api/v1/admin/import/vimeo", {
method: "POST",
body: JSON.stringify(body),
}),
importVimeoStatus: () =>
request<ArtStationImportStatus>("/api/v1/admin/import/vimeo"),
};
export interface ArtStationImportStatus {
+142
View File
@@ -25,6 +25,12 @@ export function AdminDashboard() {
const [flStatus, setFlStatus] = useState<ArtStationImportStatus | null>(null);
const flPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [vmUser, setVmUser] = useState("jmartgraphix");
const [vmDryRun, setVmDryRun] = useState(false);
const [vmPublish, setVmPublish] = useState(false);
const [vmStatus, setVmStatus] = useState<ArtStationImportStatus | null>(null);
const vmPollRef = useRef<ReturnType<typeof setInterval> | null>(null);
function refreshStats() {
Promise.all([
api.adminProjects({ perPage: 1 }),
@@ -47,9 +53,11 @@ export function AdminDashboard() {
refreshStats();
api.importArtStationStatus().then(setImportStatus).catch(() => {});
api.importFlickrStatus().then(setFlStatus).catch(() => {});
api.importVimeoStatus().then(setVmStatus).catch(() => {});
return () => {
if (pollRef.current) clearInterval(pollRef.current);
if (flPollRef.current) clearInterval(flPollRef.current);
if (vmPollRef.current) clearInterval(vmPollRef.current);
};
}, []);
@@ -145,10 +153,53 @@ export function AdminDashboard() {
}
}
function startVimeoPolling() {
if (vmPollRef.current) clearInterval(vmPollRef.current);
vmPollRef.current = setInterval(async () => {
try {
const s = await api.importVimeoStatus();
setVmStatus(s);
if (!s.running) {
if (vmPollRef.current) clearInterval(vmPollRef.current);
vmPollRef.current = null;
refreshStats();
if (s.result) {
setMsg(
`Vimeo import finished: ${s.result.imported} new, ${s.result.skipped} skipped, ${s.result.errors} errors (${s.result.found} on profile)`
);
} else if (s.error) {
setErr(s.error);
}
}
} catch {
/* keep polling */
}
}, 2000);
}
async function runVimeoImport() {
setErr(null);
setMsg(null);
try {
const res = await api.importVimeo({
username: vmUser.trim() || "jmartgraphix",
dryRun: vmDryRun,
publish: vmPublish,
});
setVmStatus(res.status);
setMsg(res.message);
if (res.status.running) startVimeoPolling();
} catch (e) {
setErr((e as Error).message);
}
}
const running = importStatus?.running;
const logLines = importStatus?.log?.slice(-40) || [];
const flRunning = flStatus?.running;
const flLogLines = flStatus?.log?.slice(-40) || [];
const vmRunning = vmStatus?.running;
const vmLogLines = vmStatus?.log?.slice(-40) || [];
return (
<div className="admin-page">
@@ -373,6 +424,97 @@ export function AdminDashboard() {
)}
</section>
<section className="admin-import" style={{ marginTop: "2.5rem", maxWidth: 720 }}>
<h2 style={{ fontSize: "1.1rem", margin: "0 0 0.35rem" }}>Import from Vimeo</h2>
<p style={{ color: "var(--text-muted)", margin: "0 0 1rem", fontSize: "0.9rem" }}>
Pull public videos from your Vimeo profile. Tagged as <strong>Video</strong> (and{" "}
<strong>3D Animation</strong> when the description suggests 3D). Embeds play in-page;
existing entries are skipped.
</p>
<div className="field">
<label htmlFor="vm-user">Vimeo username</label>
<input
id="vm-user"
value={vmUser}
onChange={(e) => setVmUser(e.target.value)}
disabled={!!vmRunning}
placeholder="jmartgraphix"
/>
</div>
<div className="admin-check-grid" style={{ marginBottom: "1rem" }}>
<label>
<input
type="checkbox"
checked={vmDryRun}
disabled={!!vmRunning}
onChange={(e) => setVmDryRun(e.target.checked)}
/>
Dry run (list only, no writes)
</label>
<label>
<input
type="checkbox"
checked={vmPublish}
disabled={!!vmRunning || vmDryRun}
onChange={(e) => setVmPublish(e.target.checked)}
/>
Publish new imports immediately
</label>
</div>
<div className="admin-page__toolbar">
<button
type="button"
className="btn btn--primary"
disabled={!!vmRunning}
onClick={runVimeoImport}
>
{vmRunning ? "Import running…" : "Import from Vimeo"}
</button>
{vmStatus?.finishedAt && !vmRunning && (
<span style={{ color: "var(--text-dim)", fontSize: "0.85rem" }}>
Last run {new Date(vmStatus.finishedAt).toLocaleString()}
</span>
)}
</div>
{vmStatus?.result && !vmRunning && (
<div
className="admin-msg admin-msg--ok"
style={{ marginTop: "1rem", whiteSpace: "pre-wrap" }}
>
Found {vmStatus.result.found} · imported {vmStatus.result.imported} · skipped{" "}
{vmStatus.result.skipped} · errors {vmStatus.result.errors}
{vmStatus.result.created.length > 0 && (
<>
{"\n"}New: {vmStatus.result.created.map((c) => c.title).join(", ")}
</>
)}
</div>
)}
{vmLogLines.length > 0 && (
<pre
style={{
marginTop: "1rem",
padding: "0.85rem 1rem",
background: "var(--bg-elevated)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-sm)",
fontSize: "0.78rem",
lineHeight: 1.45,
maxHeight: 280,
overflow: "auto",
color: "var(--text-muted)",
}}
>
{vmLogLines.join("\n")}
</pre>
)}
</section>
<section style={{ marginTop: "2.5rem" }}>
<h2 style={{ fontSize: "1rem", color: "var(--text-muted)" }}>Quick links for audiences</h2>
<ul style={{ color: "var(--text-dim)", lineHeight: 1.9 }}>