From c7ec8c21f287caf0a34f539b4cf17bb0aff70d89 Mon Sep 17 00:00:00 2001 From: Mattias Tall Date: Tue, 26 May 2026 11:47:16 +0200 Subject: [PATCH] Add paste-from-YouTube import to subscription section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alongside the CSV option, add a text area that accepts copy-pasted text from the YouTube subscriptions page. Extracts @handle from lines like '@handle•N subscribers' using regex, deduplicates, then calls follow-bulk. Button live-counts how many handles are detected as you paste. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Settings.jsx | 105 +++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 1f98968..86182e1 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -339,68 +339,113 @@ function DiagnosticSection() { } function SubscriptionImportSection() { - const [status, setStatus] = useState(null); // null | { imported, skipped, errors } + const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); + const [pasteText, setPasteText] = useState(""); + const [showPaste, setShowPaste] = useState(false); const fileRef = useRef(null); - const handleFile = async (e) => { - const file = e.target.files?.[0]; - if (!file) return; + const doImport = async (handles) => { + if (!handles.length) { + setStatus({ error: "No channel handles found." }); + return; + } setLoading(true); setStatus(null); try { - const text = await file.text(); - const lines = text.split("\n").map((l) => l.trim()).filter(Boolean); - // Skip header row (Kanal-id or Channel ID or similar) - const dataLines = lines.slice(1); - // First column is the channel ID (UCxxxxxxx) - const ids = dataLines - .map((l) => l.split(",")[0]?.replace(/^"|"$/g, "").trim()) - .filter((id) => id?.startsWith("UC")); - if (!ids.length) { - setStatus({ error: "No channel IDs found. Expected UC... IDs in the first column." }); - return; - } - const res = await followBulk(ids); + const res = await followBulk(handles); setStatus(res.data); } catch (err) { setStatus({ error: err.response?.data?.detail || err.message }); } finally { setLoading(false); - if (fileRef.current) fileRef.current.value = ""; } }; + const handleFile = async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + if (fileRef.current) fileRef.current.value = ""; + const text = await file.text(); + const lines = text.split("\n").map((l) => l.trim()).filter(Boolean); + const dataLines = lines.slice(1); + const ids = dataLines + .map((l) => l.split(",")[0]?.replace(/^"|"$/g, "").trim()) + .filter((id) => id?.startsWith("UC")); + await doImport(ids); + }; + + const handlePaste = async () => { + // Extract @handles from YouTube subscription page text — lines like "@handle•N subscribers" + const handles = [...new Set( + (pasteText.match(/@[\w.-]+(?=•)/g) || []) + )]; + await doImport(handles); + setPasteText(""); + setShowPaste(false); + }; + return (
-
-
+
+ {/* CSV import row */} +
-

Import from Google Takeout

-

- Upload the subscriptions.csv from a YouTube Google Takeout export. - Channels already followed are skipped. -

+

Import from Google Takeout CSV

+

Upload the subscriptions.csv file.

+ + {/* Paste from YouTube row */} +
+
+
+

Paste from YouTube

+

Copy your subscriptions list from YouTube and paste it here — handles starting with @ are extracted automatically.

+
+ +
+ {showPaste && ( +
+