From a2b8c2a3783aa77ef6a6153bcd7d9522c10861fe Mon Sep 17 00:00:00 2001 From: Biplob Sutradhar Date: Fri, 3 Nov 2023 12:24:32 +0600 Subject: [PATCH 1/5] search switch channel --- data/xpath.json | 8 +++--- src/content/client.ts | 62 ++++++++++++++++++++++++++++++++++--------- src/utils/xpaths.ts | 2 ++ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/data/xpath.json b/data/xpath.json index d8aec20..36a07e0 100644 --- a/data/xpath.json +++ b/data/xpath.json @@ -8,8 +8,10 @@ "IS_EXPENDEDABLE_EXPENDED_BUTTON": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/ytd-guide-entry-renderer", "THREE_LINES": "//yt-icon-button[@id='guide-button']", "DRAWER_OPENED": "//div[@id='contentContainer' and @opened]", - "ALREADY_SUBSCRIBE": "//div[@id='inner-header-container']//div[@id='notification-preference-button' and not(@hidden)]//span[text()='Subscribed']", - "SUBSCRIBE_BTN": "//div[@id='inner-header-container']/div/div[@id='subscribe-button']/ytd-subscribe-button-renderer/yt-smartimation/yt-button-shape[not(@hidden)]/button", + "ALREADY_SUBSCRIBE": "//*[contains(normalize-space(), '{{channelID}}')]/..//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", + "SUBSCRIBE_BTN": "//*[contains(normalize-space(), '{{channelID}}')]/..//div[@id='subscribe-button']//span[text()='Subscribe']/ancestor::button", "UNSUB1": "//yt-formatted-string[text()='Unsubscribe']", - "UNSUB2": "//button[@aria-label='Unsubscribe']" + "UNSUB2": "//button[@aria-label='Unsubscribe']", + "SEARCH_INPUT": "//input[@id='search']", + "NAVIGATION_PROGRESS": "//yt-page-navigation-progress[@hidden]" } diff --git a/src/content/client.ts b/src/content/client.ts index 4251ee3..ef16945 100644 --- a/src/content/client.ts +++ b/src/content/client.ts @@ -16,6 +16,46 @@ let isRunning: boolean = false; let stop: boolean = false; let xpathValues: XPathModel; +function getAlreadySubscribeXpath(channelID: string) { + return xpathValues.ALREADY_SUBSCRIBE.replace("{{channelID}}", channelID); +} + +function getSubscribeButton(channelID: string) { + return xpathValues.SUBSCRIBE_BTN.replace("{{channelID}}", channelID); +} + +async function searchChannel(channelID: string) { + const search = getXpathFromElement( + xpathValues.SEARCH_INPUT + ) as HTMLInputElement; + if (search) { + search.value = channelID; + search.dispatchEvent( + new KeyboardEvent("keydown", { key: "Enter", keyCode: 13 }) + ); + return true; + } + return false; +} + +async function switchChannel(channelID: string) { + if (await searchChannel(channelID)) { + if (await waitingForProgressEnd()) { + await readySignalSend(); + } + } +} + +async function waitingForProgressEnd() { + for (let index = 0; index < 20; index++) { + if (!isXPathExpressionExists(xpathValues.NAVIGATION_PROGRESS)) { + return true; + } + await delay(500); + } + return false; +} + function isDrawerOpened() { if (!isXPathExpressionExists(xpathValues.DRAWER_OPENED)) { // Try opening drawer @@ -257,13 +297,9 @@ async function acceptSignalSend() { }); } -function newPage(path: string) { - window.location.href = "https://youtube.com/" + path; -} - -async function isAlreadySubscribe() { +async function isAlreadySubscribe(channelID: string) { for (let index = 0; index < 2; index++) { - if (isXPathExpressionExists(xpathValues.ALREADY_SUBSCRIBE)) { + if (isXPathExpressionExists(getAlreadySubscribeXpath(channelID))) { return true; } await delay(500); @@ -272,7 +308,7 @@ async function isAlreadySubscribe() { } async function subSubNow(channelID: string) { - if (await isAlreadySubscribe()) { + if (await isAlreadySubscribe(channelID)) { await runtime.send({ type: "statusOption", status: { @@ -284,7 +320,7 @@ async function subSubNow(channelID: string) { } for (let index = 0; index < 2; index++) { - const subButton = getXpathFromElement(xpathValues.SUBSCRIBE_BTN); + const subButton = getXpathFromElement(getSubscribeButton(channelID)); if (subButton) { subButton.click(); break; @@ -292,7 +328,7 @@ async function subSubNow(channelID: string) { await delay(500); } - if (await isAlreadySubscribe()) { + if (await isAlreadySubscribe(channelID)) { await runtime.send({ type: "statusOption", status: { @@ -321,7 +357,7 @@ async function unSubSubNow(channelID: string) { }, }; - if (isXPathExpressionExists(xpathValues.SUBSCRIBE_BTN)) { + if (isXPathExpressionExists(getSubscribeButton(channelID))) { await runtime.send({ type: "statusOption", status: { @@ -332,7 +368,7 @@ async function unSubSubNow(channelID: string) { return; } - const unSubButton = getXpathFromElement(xpathValues.ALREADY_SUBSCRIBE); + const unSubButton = getXpathFromElement(getAlreadySubscribeXpath(channelID)); if (unSubButton) { unSubButton.click(); await delay(50); @@ -345,7 +381,7 @@ async function unSubSubNow(channelID: string) { const unSub2 = getXpathFromElement(xpathValues.UNSUB2); if (unSub2) { unSub2.click(); - if (isXPathExpressionExists(xpathValues.SUBSCRIBE_BTN)) { + if (isXPathExpressionExists(getSubscribeButton(channelID))) { await runtime.send({ type: "statusOption", status: { @@ -400,7 +436,7 @@ export async function parseData(dataLocal: RuntimeMessage) { ); return; } - newPage(status.msg); + await switchChannel(status.msg); break; case "subscribe": if (isRunning) { diff --git a/src/utils/xpaths.ts b/src/utils/xpaths.ts index 6e4e7e7..285a387 100644 --- a/src/utils/xpaths.ts +++ b/src/utils/xpaths.ts @@ -25,6 +25,8 @@ export const XPathModelSchema = z.object({ UNSUB2: z.string().default(xpathJson.UNSUB2), UPDATE_DATE: z.string().optional(), REMOTE_DISABLE: z.boolean().optional().default(false), + SEARCH_INPUT: z.string().default(xpathJson.SEARCH_INPUT), + NAVIGATION_PROGRESS: z.string().default(xpathJson.NAVIGATION_PROGRESS), }); export type XPathModel = z.infer; From 7bfc0a83f89fb7d0c58ef88bc59e362354186bef Mon Sep 17 00:00:00 2001 From: Biplob Sutradhar Date: Fri, 3 Nov 2023 18:26:40 +0600 Subject: [PATCH 2/5] Update xpath values and add tab keypress event. --- data/xpath.json | 6 +++--- src/content/client.ts | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/data/xpath.json b/data/xpath.json index 36a07e0..683a997 100644 --- a/data/xpath.json +++ b/data/xpath.json @@ -8,10 +8,10 @@ "IS_EXPENDEDABLE_EXPENDED_BUTTON": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/ytd-guide-entry-renderer", "THREE_LINES": "//yt-icon-button[@id='guide-button']", "DRAWER_OPENED": "//div[@id='contentContainer' and @opened]", - "ALREADY_SUBSCRIBE": "//*[contains(normalize-space(), '{{channelID}}')]/..//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", - "SUBSCRIBE_BTN": "//*[contains(normalize-space(), '{{channelID}}')]/..//div[@id='subscribe-button']//span[text()='Subscribe']/ancestor::button", + "ALREADY_SUBSCRIBE": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", + "SUBSCRIBE_BTN": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='subscribe-button']//span[text()='Subscribe']/ancestor::button", "UNSUB1": "//yt-formatted-string[text()='Unsubscribe']", "UNSUB2": "//button[@aria-label='Unsubscribe']", "SEARCH_INPUT": "//input[@id='search']", - "NAVIGATION_PROGRESS": "//yt-page-navigation-progress[@hidden]" + "NAVIGATION_PROGRESS": "//yt-page-navigation-progress[not(@hidden)]" } diff --git a/src/content/client.ts b/src/content/client.ts index ef16945..48975f7 100644 --- a/src/content/client.ts +++ b/src/content/client.ts @@ -15,6 +15,7 @@ import type { XPathModel } from "src/utils/xpaths"; let isRunning: boolean = false; let stop: boolean = false; let xpathValues: XPathModel; +let isNotTabRegister = true; function getAlreadySubscribeXpath(channelID: string) { return xpathValues.ALREADY_SUBSCRIBE.replace("{{channelID}}", channelID); @@ -29,10 +30,12 @@ async function searchChannel(channelID: string) { xpathValues.SEARCH_INPUT ) as HTMLInputElement; if (search) { + if (isNotTabRegister) { + search.dispatchEvent(new KeyboardEvent("keypress", { key: "Tab" })); + await delay(100); + } search.value = channelID; - search.dispatchEvent( - new KeyboardEvent("keydown", { key: "Enter", keyCode: 13 }) - ); + search.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 })); return true; } return false; @@ -47,10 +50,14 @@ async function switchChannel(channelID: string) { } async function waitingForProgressEnd() { - for (let index = 0; index < 20; index++) { + await delay(100); + for (let index = 0; index < 50; index++) { if (!isXPathExpressionExists(xpathValues.NAVIGATION_PROGRESS)) { return true; } + if (isNotTabRegister) { + isNotTabRegister = false; + } await delay(500); } return false; @@ -381,6 +388,7 @@ async function unSubSubNow(channelID: string) { const unSub2 = getXpathFromElement(xpathValues.UNSUB2); if (unSub2) { unSub2.click(); + await delay(50); if (isXPathExpressionExists(getSubscribeButton(channelID))) { await runtime.send({ type: "statusOption", From 9ce9f31acda86072319d720107e03adf8394cb73 Mon Sep 17 00:00:00 2001 From: Biplob Sutradhar Date: Fri, 3 Nov 2023 18:48:59 +0600 Subject: [PATCH 3/5] Update version v1.6 and xpaths --- data/xpath.json | 8 +++----- data/xpaths/v1.6.json | 17 +++++++++++++++++ manifest.json | 2 +- package.json | 2 +- src/utils/constants.ts | 5 ++--- src/utils/xpaths.ts | 2 +- 6 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 data/xpaths/v1.6.json diff --git a/data/xpath.json b/data/xpath.json index 683a997..17299f8 100644 --- a/data/xpath.json +++ b/data/xpath.json @@ -8,10 +8,8 @@ "IS_EXPENDEDABLE_EXPENDED_BUTTON": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/ytd-guide-entry-renderer", "THREE_LINES": "//yt-icon-button[@id='guide-button']", "DRAWER_OPENED": "//div[@id='contentContainer' and @opened]", - "ALREADY_SUBSCRIBE": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", - "SUBSCRIBE_BTN": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='subscribe-button']//span[text()='Subscribe']/ancestor::button", + "ALREADY_SUBSCRIBE": "//div[@id='inner-header-container']//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", + "SUBSCRIBE_BTN": "//div[@id='inner-header-container']/div/div[@id='subscribe-button']//yt-button-shape[not(@hidden) and @id='subscribe-button-shape']/button", "UNSUB1": "//yt-formatted-string[text()='Unsubscribe']", - "UNSUB2": "//button[@aria-label='Unsubscribe']", - "SEARCH_INPUT": "//input[@id='search']", - "NAVIGATION_PROGRESS": "//yt-page-navigation-progress[not(@hidden)]" + "UNSUB2": "//button[@aria-label='Unsubscribe']" } diff --git a/data/xpaths/v1.6.json b/data/xpaths/v1.6.json new file mode 100644 index 0000000..683a997 --- /dev/null +++ b/data/xpaths/v1.6.json @@ -0,0 +1,17 @@ +{ + "SUBSCRIPTIONS_SECTION": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']", + "GET_CHANNELS_WITHOUT_EXPEND": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-entry-renderer/a[@href]", + "SUB_CHANNELS_EXPENDED_ITEMS": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/div/div/ytd-guide-entry-renderer", + "GET_CHANNELS_IN_EXPEND": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/div/div/ytd-guide-entry-renderer/a[@href]", + "IS_EXPENDEDABLE": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer", + "IS_EXPENDEDABLE_EXPENDED": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer[@expanded]", + "IS_EXPENDEDABLE_EXPENDED_BUTTON": "//*[contains(normalize-space(), 'Subscriptions')]/following-sibling::div[@id='items']/ytd-guide-collapsible-entry-renderer/ytd-guide-entry-renderer", + "THREE_LINES": "//yt-icon-button[@id='guide-button']", + "DRAWER_OPENED": "//div[@id='contentContainer' and @opened]", + "ALREADY_SUBSCRIBE": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='notification-preference-button' and not(@invisible)]//span[text()='Subscribed']", + "SUBSCRIBE_BTN": "//span[translate(.,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='{{channelID}}']/ancestor::div[@id='info-section']//div[@id='subscribe-button']//span[text()='Subscribe']/ancestor::button", + "UNSUB1": "//yt-formatted-string[text()='Unsubscribe']", + "UNSUB2": "//button[@aria-label='Unsubscribe']", + "SEARCH_INPUT": "//input[@id='search']", + "NAVIGATION_PROGRESS": "//yt-page-navigation-progress[not(@hidden)]" +} diff --git a/manifest.json b/manifest.json index 7045cd6..b8995ef 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Youtube Subscriptions Transfer", "description": "Transferring subscriptions from one YouTube account to another", - "version": "1.5", + "version": "1.6", "manifest_version": 3, "icons": { "16": "src/assets/icons/icon16.png", diff --git a/package.json b/package.json index 134d173..c54b03a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "youtube-subscriptions-transfer", - "version": "1.5", + "version": "1.6", "type": "module", "repository": { "type": "git", diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9a720ec..95fa900 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,11 +1,10 @@ export const STORIES_URL = ["https://www.youtube.com/"]; export const DEFAULT_STATUS_MSG = "Ready for request"; export const APP_NAME = "Youtube Subscriptions Transfer"; -export const VERSION = "v1.5"; +export const VERSION = "v1.6"; export const REPO_URL = "https://github.com/biplobsd/yst"; -export const XPATH_URL = - "https://raw.githubusercontent.com/biplobsd/yst/main/data/xpath.json"; +export const XPATH_URL = `https://raw.githubusercontent.com/biplobsd/yst/main/data/xpaths/${VERSION}.json`; export const CHANNEL_PATHS_KEY = "channelPaths"; export const XPATH_VALUES_KEY = "xpathValues"; diff --git a/src/utils/xpaths.ts b/src/utils/xpaths.ts index 285a387..acf69ea 100644 --- a/src/utils/xpaths.ts +++ b/src/utils/xpaths.ts @@ -1,4 +1,4 @@ -import xpathJson from "../../data/xpath.json"; +import xpathJson from "../../data/xpaths/v1.6.json"; import { z } from "zod"; export const XPathModelSchema = z.object({ From ef1a3de7440026503aca7174a7dfa1b7085e7db6 Mon Sep 17 00:00:00 2001 From: Biplob Sutradhar Date: Fri, 3 Nov 2023 21:10:08 +0600 Subject: [PATCH 4/5] Refactor ChannelRawSchema to handle optional customUrl --- src/utils/schema.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils/schema.ts b/src/utils/schema.ts index 7061d82..95bce25 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -32,12 +32,16 @@ export const ChannelRawSchema = z items: z .object({ snippet: z.object({ - customUrl: z.string(), + customUrl: z.string().optional(), }), }) .array(), }) - .transform((x) => x.items.map((y) => y.snippet.customUrl)); + .transform((x) => + x.items + .filter((y) => y.snippet.customUrl !== undefined) + .map((y) => y.snippet.customUrl!) + ); export const SubscriptionsListSchema = z .object({ From 8e9eab78c43c2982b8ff5b271747b06650dca7da Mon Sep 17 00:00:00 2001 From: Biplob Sutradhar Date: Fri, 3 Nov 2023 22:09:36 +0600 Subject: [PATCH 5/5] Add documentation links to various components --- src/components/Docs_Link.svelte | 10 ++++++++++ src/components/Footer.svelte | 3 ++- src/components/api/Select_Account.svelte | 6 +++++- src/components/data/Zip_Reader.svelte | 6 +++++- src/components/icons/Question_circle_Icon.svelte | 14 ++++++++++++++ src/components/pages/Home.svelte | 10 ++++++++-- src/components/setting/Mode_Switch.svelte | 8 ++++++-- src/components/setting/api/API_Delay.svelte | 7 ++++++- src/components/setting/xpath/Update_Xpath.svelte | 6 +++++- src/utils/docs.ts | 15 +++++++++++++++ 10 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 src/components/Docs_Link.svelte create mode 100644 src/components/icons/Question_circle_Icon.svelte create mode 100644 src/utils/docs.ts diff --git a/src/components/Docs_Link.svelte b/src/components/Docs_Link.svelte new file mode 100644 index 0000000..122200e --- /dev/null +++ b/src/components/Docs_Link.svelte @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/components/Footer.svelte b/src/components/Footer.svelte index 319b9ba..a70d738 100644 --- a/src/components/Footer.svelte +++ b/src/components/Footer.svelte @@ -5,6 +5,7 @@ import { onMount } from "svelte"; import GithubMark from "src/assets/icons/github-mark.png"; import GithubMarkWhite from "src/assets/icons/github-mark-white.png"; + import { docs } from "src/utils/docs"; let isLight = false; @@ -20,7 +21,7 @@ class="btn btn-xs normal-case" target="_blank" rel="noreferrer" - href={REPO_URL} + href={docs.README} > -
Account
+
+ Account +
diff --git a/src/components/data/Zip_Reader.svelte b/src/components/data/Zip_Reader.svelte index f1823a9..1b38d8e 100644 --- a/src/components/data/Zip_Reader.svelte +++ b/src/components/data/Zip_Reader.svelte @@ -9,6 +9,8 @@ import { ChannelRawSchema } from "src/utils/schema"; import qs from "qs"; import toast from "svelte-french-toast"; + import DocsLink from "../Docs_Link.svelte"; + import { docs } from "src/utils/docs"; export let channelsIdsTakeoutSave: (channelIDs: string[]) => void; let files: FileList | null = null; @@ -121,7 +123,9 @@
-
Takeout import
+
+ Takeout import +
{#if isLoading}
+ + diff --git a/src/components/pages/Home.svelte b/src/components/pages/Home.svelte index e573208..ac92c2d 100644 --- a/src/components/pages/Home.svelte +++ b/src/components/pages/Home.svelte @@ -20,6 +20,8 @@ import Timer from "../Timer.svelte"; import { channelPathsSchema } from "src/utils/schema"; import ZipReader from "../data/Zip_Reader.svelte"; + import DocsLink from "../Docs_Link.svelte"; + import { docs } from "src/utils/docs"; let channelPaths: string[] = []; let xpathValues: XPathModel | undefined = undefined; @@ -437,7 +439,9 @@ {#if isRightSiteNow}
-
Data
+
+ Data +
@@ -585,7 +589,9 @@
-
Actions
+
+ Actions +
{#if actionName !== ""}
import { MODE_DEFAULT } from "src/utils/default"; + import { docs } from "src/utils/docs"; import { modeWritable, type MODE } from "src/utils/storage"; import { onMount } from "svelte"; + import DocsLink from "../Docs_Link.svelte"; let localMode: MODE = MODE_DEFAULT; @@ -12,8 +14,10 @@ }); -
- Select Mode +
+ Select Mode