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:
@@ -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 {
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user