yt-grabber 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/automations/Helpers.ts +15 -0
- package/src/automations/Selectors.ts +10 -0
- package/src/automations/Youtube.ts +12 -12
- package/src/automations/YoutubeAlbums.ts +128 -0
- package/src/automations/YoutubeArtists.ts +153 -0
- package/src/automations/YoutubeSongs.ts +130 -0
- package/src/common/Helpers.ts +5 -1
- package/src/common/Media.ts +7 -0
- package/src/common/Messaging.ts +18 -0
- package/src/common/Store.ts +11 -5
- package/src/common/Youtube.ts +1 -0
- package/src/components/youtube/inputModePicker/InputModePicker.styl +21 -0
- package/src/components/youtube/inputModePicker/InputModePicker.tsx +107 -0
- package/src/components/youtube/inputPanel/InputPanel.styl +0 -5
- package/src/components/youtube/inputPanel/InputPanel.tsx +51 -19
- package/src/index.ts +71 -4
- package/src/resources/locales/de-DE/help.json +12 -6
- package/src/resources/locales/de-DE/translation.json +6 -1
- package/src/resources/locales/en-GB/help.json +12 -6
- package/src/resources/locales/en-GB/translation.json +6 -1
- package/src/resources/locales/pl-PL/help.json +12 -6
- package/src/resources/locales/pl-PL/translation.json +6 -1
- package/src/views/home/HomeView.styl +1 -0
- package/src/views/home/HomeView.tsx +138 -3
- package/src/views/settings/SettingsView.tsx +4 -4
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {ElementHandle, Page} from "puppeteer";
|
|
2
|
+
|
|
3
|
+
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
4
|
+
|
|
5
|
+
export const navigateToPage = async (url: string, page: Page, timeout = puppeteerOptions.timeout) => {
|
|
6
|
+
await page.goto(url, {
|
|
7
|
+
waitUntil: ["networkidle0", "domcontentloaded", "load"],
|
|
8
|
+
timeout,
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const clearInput = async (input: ElementHandle<Element>, page: Page) => {
|
|
13
|
+
await input.click({clickCount: 3});
|
|
14
|
+
await page.keyboard.press("Backspace");
|
|
15
|
+
};
|
|
@@ -5,3 +5,13 @@ export const AlbumFilterSelector = "//ytmusic-app-layout//div[@id='content']/ytm
|
|
|
5
5
|
export const AlbumLinkSelector = "//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer/div[@id='contents']/ytmusic-grid-renderer/div[@id='items']/*[contains(@class, 'ytmusic-grid-renderer')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]";
|
|
6
6
|
|
|
7
7
|
export const AlbumsDirectLinkSelector = "//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer//div[@id='content-group']//div[contains(@class, 'header-renderer')]/yt-formatted-string[contains(text(), 'Album')]//ancestor::div[contains(@class, 'ytmusic-shelf')]//div[@id='items-wrapper']/ul/*[contains(@class, 'ytmusic-carousel')]//a[contains(@class, 'yt-simple-endpoint') and contains(@class, 'yt-formatted-string')]";
|
|
8
|
+
|
|
9
|
+
export const YtMusicSearchInputSelector = "//ytmusic-app-layout//ytmusic-search-box//input[@id='input']";
|
|
10
|
+
|
|
11
|
+
export const YtMusicArtistsChipSelector = "//ytmusic-app-layout//ytmusic-search-page//div[contains(@class, 'content')]//ytmusic-section-list-renderer//ytmusic-chip-cloud-renderer//iron-selector[@id='chips']//a[contains(@class, 'ytmusic-chip-cloud-chip-renderer')]//yt-formatted-string[contains(text(), 'Wykonawcy') or contains(text(), 'Artists')]//ancestor::a";
|
|
12
|
+
|
|
13
|
+
export const YtMusicAlbumsChipSelector = "//ytmusic-app-layout//ytmusic-search-page//div[contains(@class, 'content')]//ytmusic-section-list-renderer//ytmusic-chip-cloud-renderer//iron-selector[@id='chips']//a[contains(@class, 'ytmusic-chip-cloud-chip-renderer')]//yt-formatted-string[contains(text(), 'Albumy') or contains(text(), 'Albums')]//ancestor::a";
|
|
14
|
+
|
|
15
|
+
export const YtMusicSongsChipSelector = "//ytmusic-app-layout//ytmusic-search-page//div[contains(@class, 'content')]//ytmusic-section-list-renderer//ytmusic-chip-cloud-renderer//iron-selector[@id='chips']//a[contains(@class, 'ytmusic-chip-cloud-chip-renderer')]//yt-formatted-string[contains(text(), 'Utwory') or contains(text(), 'Songs')]//ancestor::a";
|
|
16
|
+
|
|
17
|
+
export const YtMusicSearchResultsSelector = "//ytmusic-app-layout//div[@id='content']//ytmusic-search-page//div[@id='contents']//ytmusic-shelf-renderer//div[@id='contents']//ytmusic-responsive-list-item-renderer/a";
|
|
@@ -15,6 +15,7 @@ import {waitFor} from "../common/Helpers";
|
|
|
15
15
|
import {GetYoutubeUrlParams, GetYoutubeUrlResult} from "../common/Messaging";
|
|
16
16
|
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
17
17
|
import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
|
|
18
|
+
import {navigateToPage} from "./Helpers";
|
|
18
19
|
import {
|
|
19
20
|
AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector
|
|
20
21
|
} from "./Selectors";
|
|
@@ -25,12 +26,7 @@ let reporter: IReporter<GetYoutubeUrlResult>;
|
|
|
25
26
|
|
|
26
27
|
puppeteer.use(StealthPlugin());
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
await page.goto(url, {
|
|
30
|
-
waitUntil: ["networkidle0", "domcontentloaded", "load"],
|
|
31
|
-
timeout: puppeteerOptions.timeout,
|
|
32
|
-
});
|
|
33
|
-
};
|
|
29
|
+
|
|
34
30
|
|
|
35
31
|
export const execute = async (
|
|
36
32
|
params: GetYoutubeUrlParams,
|
|
@@ -64,18 +60,18 @@ export const execute = async (
|
|
|
64
60
|
await page.setCookie(...cachedCookies);
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
await navigateToPage(params.url);
|
|
63
|
+
await navigateToPage(params.url, page);
|
|
68
64
|
|
|
69
65
|
const process = async (urlToProcess: string) => {
|
|
70
66
|
const results: string[] = [];
|
|
71
67
|
|
|
72
68
|
try {
|
|
73
|
-
await navigateToPage(urlToProcess);
|
|
69
|
+
await navigateToPage(urlToProcess, page);
|
|
74
70
|
|
|
75
71
|
const element = await page.waitForSelector(`::-p-xpath(${AlbumsHrefSelector})`, {timeout: 1000});
|
|
76
72
|
const albumsUrl = await element.evaluate((el) => el.getAttribute("href"));
|
|
77
73
|
|
|
78
|
-
await navigateToPage(`${params.url}/${albumsUrl}
|
|
74
|
+
await navigateToPage(`${params.url}/${albumsUrl}`, page);
|
|
79
75
|
const albumFilterButton = await page.waitForSelector(`::-p-xpath(${AlbumFilterSelector})`, {timeout: 1000});
|
|
80
76
|
|
|
81
77
|
albumFilterButton.click();
|
|
@@ -115,7 +111,11 @@ export const execute = async (
|
|
|
115
111
|
if (error instanceof TimeoutError) {
|
|
116
112
|
result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
|
|
117
113
|
} else {
|
|
118
|
-
|
|
114
|
+
if (error.message === "Navigating frame was detached") {
|
|
115
|
+
result.warnings.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
116
|
+
} else {
|
|
117
|
+
result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
118
|
+
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
reporter.finish("done", result);
|
|
@@ -131,8 +131,8 @@ const closeResources = async () => {
|
|
|
131
131
|
};
|
|
132
132
|
|
|
133
133
|
export const cancel = async () => {
|
|
134
|
-
browser.close();
|
|
135
|
-
browser.disconnect();
|
|
134
|
+
await browser.close();
|
|
135
|
+
await browser.disconnect();
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
export default execute;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import {i18n as i18next} from "i18next";
|
|
3
|
+
import _forEach from "lodash/forEach";
|
|
4
|
+
import _includes from "lodash/includes";
|
|
5
|
+
import _isEmpty from "lodash/isEmpty";
|
|
6
|
+
import _map from "lodash/map";
|
|
7
|
+
import _merge from "lodash/merge";
|
|
8
|
+
import _replace from "lodash/replace";
|
|
9
|
+
import {Browser, LaunchOptions, Page, TimeoutError} from "puppeteer";
|
|
10
|
+
import puppeteer from "puppeteer-extra";
|
|
11
|
+
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
12
|
+
|
|
13
|
+
import {getProfilePath} from "../common/FileSystem";
|
|
14
|
+
import {waitFor} from "../common/Helpers";
|
|
15
|
+
import {GetYoutubeAlbumsParams, GetYoutubeUrlResult} from "../common/Messaging";
|
|
16
|
+
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
17
|
+
import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
|
|
18
|
+
import {clearInput, navigateToPage} from "./Helpers";
|
|
19
|
+
import {
|
|
20
|
+
YtMusicAlbumsChipSelector, YtMusicSearchInputSelector, YtMusicSearchResultsSelector
|
|
21
|
+
} from "./Selectors";
|
|
22
|
+
|
|
23
|
+
let page: Page;
|
|
24
|
+
let browser: Browser;
|
|
25
|
+
let reporter: IReporter<GetYoutubeUrlResult>;
|
|
26
|
+
|
|
27
|
+
puppeteer.use(StealthPlugin());
|
|
28
|
+
|
|
29
|
+
export const execute = async (
|
|
30
|
+
params: GetYoutubeAlbumsParams,
|
|
31
|
+
options: LaunchOptions,
|
|
32
|
+
i18n: i18next,
|
|
33
|
+
onProgress: (data: ProgressInfo<GetYoutubeUrlResult>) => void,
|
|
34
|
+
) => {
|
|
35
|
+
try {
|
|
36
|
+
const result: GetYoutubeUrlResult = {warnings: [], errors: [], urls: []};
|
|
37
|
+
const userAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.3";
|
|
38
|
+
|
|
39
|
+
await i18n.changeLanguage(params.lang);
|
|
40
|
+
|
|
41
|
+
reporter = new Reporter(onProgress);
|
|
42
|
+
reporter.start(i18n.t("starting"));
|
|
43
|
+
browser = await puppeteer.launch(_merge(puppeteerOptions, options));
|
|
44
|
+
[page] = await browser.pages();
|
|
45
|
+
|
|
46
|
+
await page.setUserAgent(userAgent);
|
|
47
|
+
|
|
48
|
+
const cachedCookies = fs.readJSONSync(getProfilePath() + "/cookies.json", {throws: false});
|
|
49
|
+
|
|
50
|
+
if (_isEmpty(cachedCookies)) {
|
|
51
|
+
await waitFor(3000);
|
|
52
|
+
const pageCookies = await page.cookies();
|
|
53
|
+
|
|
54
|
+
fs.writeJSONSync(getProfilePath() + "/cookies.json", pageCookies, {spaces: 2});
|
|
55
|
+
|
|
56
|
+
await page.setCookie(...pageCookies);
|
|
57
|
+
} else {
|
|
58
|
+
await page.setCookie(...cachedCookies);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await navigateToPage(params.url, page);
|
|
62
|
+
|
|
63
|
+
const process = async (album: string) => {
|
|
64
|
+
const results: string[] = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
68
|
+
await clearInput(searchInput, page);
|
|
69
|
+
await searchInput.type(album);
|
|
70
|
+
page.keyboard.press("Enter");
|
|
71
|
+
await page.waitForNetworkIdle();
|
|
72
|
+
|
|
73
|
+
const albumsChip = await page.waitForSelector(`::-p-xpath(${YtMusicAlbumsChipSelector})`, {timeout: 1000});
|
|
74
|
+
|
|
75
|
+
albumsChip.click();
|
|
76
|
+
await page.waitForNetworkIdle();
|
|
77
|
+
await page.waitForSelector(`::-p-xpath(${YtMusicSearchResultsSelector})`, {timeout: 1000});
|
|
78
|
+
|
|
79
|
+
const albumsElements = await page.$$(`::-p-xpath(${YtMusicSearchResultsSelector})`);
|
|
80
|
+
const albumEl = albumsElements[0];
|
|
81
|
+
const albumUrl = await albumEl.evaluate((el) => el.getAttribute("href"));
|
|
82
|
+
|
|
83
|
+
results.push(`${params.url}/${albumUrl}`);
|
|
84
|
+
|
|
85
|
+
return results;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
for (const a of params.albums) {
|
|
92
|
+
const data = await process(a);
|
|
93
|
+
|
|
94
|
+
result.urls.push(...data);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
reporter.finish("done", result);
|
|
98
|
+
} catch (error: any) {
|
|
99
|
+
const result: GetYoutubeUrlResult = {errors: []};
|
|
100
|
+
|
|
101
|
+
if (error instanceof TimeoutError) {
|
|
102
|
+
result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
|
|
103
|
+
} else {
|
|
104
|
+
if (error.message === "Navigating frame was detached") {
|
|
105
|
+
result.warnings.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
106
|
+
} else {
|
|
107
|
+
result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
reporter.finish("done", result);
|
|
112
|
+
console.error("Execution failed at stage: ", error.stack);
|
|
113
|
+
} finally {
|
|
114
|
+
await closeResources();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const closeResources = async () => {
|
|
119
|
+
if (page) await page.close();
|
|
120
|
+
if (browser) await browser.close();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const cancel = async () => {
|
|
124
|
+
await browser.close();
|
|
125
|
+
await browser.disconnect();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default execute;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import {i18n as i18next} from "i18next";
|
|
3
|
+
import _forEach from "lodash/forEach";
|
|
4
|
+
import _includes from "lodash/includes";
|
|
5
|
+
import _isEmpty from "lodash/isEmpty";
|
|
6
|
+
import _map from "lodash/map";
|
|
7
|
+
import _merge from "lodash/merge";
|
|
8
|
+
import _replace from "lodash/replace";
|
|
9
|
+
import {Browser, LaunchOptions, Page, TimeoutError} from "puppeteer";
|
|
10
|
+
import puppeteer from "puppeteer-extra";
|
|
11
|
+
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
12
|
+
|
|
13
|
+
import {getProfilePath} from "../common/FileSystem";
|
|
14
|
+
import {waitFor} from "../common/Helpers";
|
|
15
|
+
import {GetYoutubeArtistsParams, GetYoutubeUrlResult} from "../common/Messaging";
|
|
16
|
+
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
17
|
+
import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
|
|
18
|
+
import {clearInput, navigateToPage} from "./Helpers";
|
|
19
|
+
import {
|
|
20
|
+
AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector,
|
|
21
|
+
YtMusicArtistsChipSelector, YtMusicSearchInputSelector, YtMusicSearchResultsSelector
|
|
22
|
+
} from "./Selectors";
|
|
23
|
+
|
|
24
|
+
let page: Page;
|
|
25
|
+
let browser: Browser;
|
|
26
|
+
let reporter: IReporter<GetYoutubeUrlResult>;
|
|
27
|
+
|
|
28
|
+
puppeteer.use(StealthPlugin());
|
|
29
|
+
|
|
30
|
+
export const execute = async (
|
|
31
|
+
params: GetYoutubeArtistsParams,
|
|
32
|
+
options: LaunchOptions,
|
|
33
|
+
i18n: i18next,
|
|
34
|
+
onProgress: (data: ProgressInfo<GetYoutubeUrlResult>) => void,
|
|
35
|
+
) => {
|
|
36
|
+
try {
|
|
37
|
+
const result: GetYoutubeUrlResult = {warnings: [], errors: [], urls: []};
|
|
38
|
+
const userAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.3";
|
|
39
|
+
|
|
40
|
+
await i18n.changeLanguage(params.lang);
|
|
41
|
+
|
|
42
|
+
reporter = new Reporter(onProgress);
|
|
43
|
+
reporter.start(i18n.t("starting"));
|
|
44
|
+
browser = await puppeteer.launch(_merge(puppeteerOptions, options));
|
|
45
|
+
[page] = await browser.pages();
|
|
46
|
+
|
|
47
|
+
await page.setUserAgent(userAgent);
|
|
48
|
+
|
|
49
|
+
const cachedCookies = fs.readJSONSync(getProfilePath() + "/cookies.json", {throws: false});
|
|
50
|
+
|
|
51
|
+
if (_isEmpty(cachedCookies)) {
|
|
52
|
+
await waitFor(3000);
|
|
53
|
+
const pageCookies = await page.cookies();
|
|
54
|
+
|
|
55
|
+
fs.writeJSONSync(getProfilePath() + "/cookies.json", pageCookies, {spaces: 2});
|
|
56
|
+
|
|
57
|
+
await page.setCookie(...pageCookies);
|
|
58
|
+
} else {
|
|
59
|
+
await page.setCookie(...cachedCookies);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await navigateToPage(params.url, page);
|
|
63
|
+
|
|
64
|
+
const process = async (artist: string) => {
|
|
65
|
+
const results: string[] = [];
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
69
|
+
await clearInput(searchInput, page);
|
|
70
|
+
await searchInput.type(artist);
|
|
71
|
+
page.keyboard.press("Enter");
|
|
72
|
+
await page.waitForNetworkIdle();
|
|
73
|
+
|
|
74
|
+
const artistsChip = await page.waitForSelector(`::-p-xpath(${YtMusicArtistsChipSelector})`, {timeout: 1000});
|
|
75
|
+
|
|
76
|
+
artistsChip.click();
|
|
77
|
+
await page.waitForNetworkIdle();
|
|
78
|
+
|
|
79
|
+
await page.waitForSelector(`::-p-xpath(${YtMusicSearchResultsSelector})`, {timeout: 1000});
|
|
80
|
+
|
|
81
|
+
const artistsElements = await page.$$(`::-p-xpath(${YtMusicSearchResultsSelector})`);
|
|
82
|
+
const artistEl = artistsElements[0];
|
|
83
|
+
const artistChannelUrl = await artistEl.evaluate((el) => el.getAttribute("href"));
|
|
84
|
+
|
|
85
|
+
await navigateToPage(`${params.url}/${artistChannelUrl}`, page);
|
|
86
|
+
await page.waitForNetworkIdle();
|
|
87
|
+
|
|
88
|
+
const element = await page.waitForSelector(`::-p-xpath(${AlbumsHrefSelector})`, {timeout: 1000});
|
|
89
|
+
const albumsUrl = await element.evaluate((el) => el.getAttribute("href"));
|
|
90
|
+
|
|
91
|
+
await navigateToPage(`${params.url}/${albumsUrl}`, page);
|
|
92
|
+
|
|
93
|
+
const albumFilterButton = await page.waitForSelector(`::-p-xpath(${AlbumFilterSelector})`, {timeout: 1000});
|
|
94
|
+
|
|
95
|
+
albumFilterButton.click();
|
|
96
|
+
await page.waitForNetworkIdle();
|
|
97
|
+
|
|
98
|
+
const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
|
|
99
|
+
|
|
100
|
+
for (const item of items) {
|
|
101
|
+
results.push(`${params.url}/${item}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const items = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
|
|
107
|
+
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
results.push(`${params.url}/${item}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const a of params.artists) {
|
|
117
|
+
const data = await process(a);
|
|
118
|
+
|
|
119
|
+
result.urls.push(...data);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
reporter.finish("done", result);
|
|
123
|
+
} catch (error: any) {
|
|
124
|
+
const result: GetYoutubeUrlResult = {errors: []};
|
|
125
|
+
|
|
126
|
+
if (error instanceof TimeoutError) {
|
|
127
|
+
result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
|
|
128
|
+
} else {
|
|
129
|
+
if (error.message === "Navigating frame was detached") {
|
|
130
|
+
result.warnings.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
131
|
+
} else {
|
|
132
|
+
result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
reporter.finish("done", result);
|
|
137
|
+
console.error("Execution failed at stage: ", error.stack);
|
|
138
|
+
} finally {
|
|
139
|
+
await closeResources();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const closeResources = async () => {
|
|
144
|
+
if (page) await page.close();
|
|
145
|
+
if (browser) await browser.close();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const cancel = async () => {
|
|
149
|
+
await browser.close();
|
|
150
|
+
await browser.disconnect();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export default execute;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import {i18n as i18next} from "i18next";
|
|
3
|
+
import _forEach from "lodash/forEach";
|
|
4
|
+
import _includes from "lodash/includes";
|
|
5
|
+
import _isEmpty from "lodash/isEmpty";
|
|
6
|
+
import _map from "lodash/map";
|
|
7
|
+
import _merge from "lodash/merge";
|
|
8
|
+
import _replace from "lodash/replace";
|
|
9
|
+
import {Browser, LaunchOptions, Page, TimeoutError} from "puppeteer";
|
|
10
|
+
import puppeteer from "puppeteer-extra";
|
|
11
|
+
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
12
|
+
|
|
13
|
+
import {getProfilePath} from "../common/FileSystem";
|
|
14
|
+
import {waitFor} from "../common/Helpers";
|
|
15
|
+
import {GetYoutubeSongsParams, GetYoutubeUrlResult} from "../common/Messaging";
|
|
16
|
+
import puppeteerOptions from "../common/PuppeteerOptions";
|
|
17
|
+
import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
|
|
18
|
+
import {clearInput, navigateToPage} from "./Helpers";
|
|
19
|
+
import {
|
|
20
|
+
YtMusicSearchInputSelector, YtMusicSearchResultsSelector, YtMusicSongsChipSelector
|
|
21
|
+
} from "./Selectors";
|
|
22
|
+
|
|
23
|
+
let page: Page;
|
|
24
|
+
let browser: Browser;
|
|
25
|
+
let reporter: IReporter<GetYoutubeUrlResult>;
|
|
26
|
+
|
|
27
|
+
puppeteer.use(StealthPlugin());
|
|
28
|
+
|
|
29
|
+
export const execute = async (
|
|
30
|
+
params: GetYoutubeSongsParams,
|
|
31
|
+
options: LaunchOptions,
|
|
32
|
+
i18n: i18next,
|
|
33
|
+
onProgress: (data: ProgressInfo<GetYoutubeUrlResult>) => void,
|
|
34
|
+
) => {
|
|
35
|
+
try {
|
|
36
|
+
const result: GetYoutubeUrlResult = {warnings: [], errors: [], urls: []};
|
|
37
|
+
const userAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.3";
|
|
38
|
+
|
|
39
|
+
await i18n.changeLanguage(params.lang);
|
|
40
|
+
|
|
41
|
+
reporter = new Reporter(onProgress);
|
|
42
|
+
reporter.start(i18n.t("starting"));
|
|
43
|
+
browser = await puppeteer.launch(_merge(puppeteerOptions, options));
|
|
44
|
+
[page] = await browser.pages();
|
|
45
|
+
|
|
46
|
+
await page.setUserAgent(userAgent);
|
|
47
|
+
|
|
48
|
+
const cachedCookies = fs.readJSONSync(getProfilePath() + "/cookies.json", {throws: false});
|
|
49
|
+
|
|
50
|
+
if (_isEmpty(cachedCookies)) {
|
|
51
|
+
await waitFor(3000);
|
|
52
|
+
const pageCookies = await page.cookies();
|
|
53
|
+
|
|
54
|
+
fs.writeJSONSync(getProfilePath() + "/cookies.json", pageCookies, {spaces: 2});
|
|
55
|
+
|
|
56
|
+
await page.setCookie(...pageCookies);
|
|
57
|
+
} else {
|
|
58
|
+
await page.setCookie(...cachedCookies);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await navigateToPage(params.url, page);
|
|
62
|
+
|
|
63
|
+
const process = async (song: string) => {
|
|
64
|
+
const results: string[] = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
|
|
68
|
+
await clearInput(searchInput, page);
|
|
69
|
+
await searchInput.type(song);
|
|
70
|
+
page.keyboard.press("Enter");
|
|
71
|
+
await page.waitForNetworkIdle();
|
|
72
|
+
|
|
73
|
+
const songsChip = await page.waitForSelector(`::-p-xpath(${YtMusicSongsChipSelector})`, {timeout: 1000});
|
|
74
|
+
|
|
75
|
+
songsChip.click();
|
|
76
|
+
await page.waitForNetworkIdle();
|
|
77
|
+
|
|
78
|
+
await page.waitForSelector(`::-p-xpath(${YtMusicSearchResultsSelector})`, {timeout: 1000});
|
|
79
|
+
|
|
80
|
+
const songsElements = await page.$$(`::-p-xpath(${YtMusicSearchResultsSelector})`);
|
|
81
|
+
const songEl = songsElements[0];
|
|
82
|
+
const songUrl = await songEl.evaluate((el) => el.getAttribute("href"));
|
|
83
|
+
|
|
84
|
+
results.push(`${params.url}/${songUrl}`);
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const item of params.songs) {
|
|
94
|
+
const data = await process(item);
|
|
95
|
+
|
|
96
|
+
result.urls.push(...data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
reporter.finish("done", result);
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
const result: GetYoutubeUrlResult = {errors: []};
|
|
102
|
+
|
|
103
|
+
if (error instanceof TimeoutError) {
|
|
104
|
+
result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
|
|
105
|
+
} else {
|
|
106
|
+
if (error.message === "Navigating frame was detached") {
|
|
107
|
+
result.warnings.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
108
|
+
} else {
|
|
109
|
+
result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
reporter.finish("done", result);
|
|
114
|
+
console.error("Execution failed at stage: ", error.stack);
|
|
115
|
+
} finally {
|
|
116
|
+
await closeResources();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const closeResources = async () => {
|
|
121
|
+
if (page) await page.close();
|
|
122
|
+
if (browser) await browser.close();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const cancel = async () => {
|
|
126
|
+
await browser.close();
|
|
127
|
+
await browser.disconnect();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default execute;
|
package/src/common/Helpers.ts
CHANGED
|
@@ -42,15 +42,19 @@ export const waitFor = (miliseconds: number) => new Promise((resolve) => setTime
|
|
|
42
42
|
export const getUrlType = (url: string) => {
|
|
43
43
|
const artistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/|youtu\.be\/)?(channel)/;
|
|
44
44
|
const playlistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/(?:playlist\?list=|watch\?.*?\blist=)([a-zA-Z0-9_-]+)/;
|
|
45
|
+
const trackRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/watch\?.*?\bv=([a-zA-Z0-9_-]+)/;
|
|
45
46
|
|
|
46
47
|
const isArtist = artistRegex.test(url);
|
|
47
48
|
const isPlaylist = playlistRegex.test(url);
|
|
49
|
+
const isTrack = trackRegex.test(url);
|
|
48
50
|
|
|
49
51
|
if (isArtist) {
|
|
50
52
|
return UrlType.Artist;
|
|
51
53
|
} else if (isPlaylist) {
|
|
52
54
|
return UrlType.Playlist;
|
|
55
|
+
} else if (isTrack) {
|
|
56
|
+
return UrlType.Track;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
return UrlType.
|
|
59
|
+
return UrlType.Other;
|
|
56
60
|
};
|
package/src/common/Media.ts
CHANGED
package/src/common/Messaging.ts
CHANGED
|
@@ -31,6 +31,24 @@ export type GetYoutubeUrlParams = {
|
|
|
31
31
|
artistUrls?: string[];
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
export type GetYoutubeArtistsParams = {
|
|
35
|
+
artists: string[];
|
|
36
|
+
lang: string;
|
|
37
|
+
url: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type GetYoutubeAlbumsParams = {
|
|
41
|
+
albums: string[];
|
|
42
|
+
lang: string;
|
|
43
|
+
url: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type GetYoutubeSongsParams = {
|
|
47
|
+
songs: string[];
|
|
48
|
+
lang: string;
|
|
49
|
+
url: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
34
52
|
export type GetYoutubeUrlResult = {
|
|
35
53
|
errors?: LogMessage[];
|
|
36
54
|
warnings?: LogMessage[];
|
package/src/common/Store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Schema} from "electron-store";
|
|
2
2
|
import _values from "lodash/values";
|
|
3
3
|
|
|
4
|
-
import {FormatScope} from "./Media";
|
|
4
|
+
import {FormatScope, InputMode} from "./Media";
|
|
5
5
|
|
|
6
6
|
export type ApplicationOptions = {
|
|
7
7
|
youtubeUrl?: string;
|
|
@@ -17,7 +17,8 @@ export type ApplicationOptions = {
|
|
|
17
17
|
urls?: string[];
|
|
18
18
|
language?: string;
|
|
19
19
|
alwaysOverwrite?: boolean;
|
|
20
|
-
|
|
20
|
+
enableInputMode?: boolean;
|
|
21
|
+
inputMode?: InputMode;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export interface IStore {
|
|
@@ -96,10 +97,15 @@ export const StoreSchema: Schema<IStore> = {
|
|
|
96
97
|
type: "boolean",
|
|
97
98
|
default: false
|
|
98
99
|
},
|
|
99
|
-
|
|
100
|
+
enableInputMode: {
|
|
100
101
|
type: "boolean",
|
|
101
|
-
default: true
|
|
102
|
-
}
|
|
102
|
+
default: true,
|
|
103
|
+
},
|
|
104
|
+
inputMode: {
|
|
105
|
+
type: "string",
|
|
106
|
+
default: InputMode.Auto,
|
|
107
|
+
enum: _values(InputMode),
|
|
108
|
+
},
|
|
103
109
|
},
|
|
104
110
|
default: {},
|
|
105
111
|
},
|
package/src/common/Youtube.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@import "../../../styles/mixins.styl"
|
|
2
|
+
|
|
3
|
+
.input-mode-picker {
|
|
4
|
+
.button {
|
|
5
|
+
padding: 6px 0px 6px 6px;
|
|
6
|
+
|
|
7
|
+
.icon {
|
|
8
|
+
font-size: 1.7rem;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.popper {
|
|
13
|
+
z-index: 99;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.menu-item {
|
|
17
|
+
column-gap: 5px;
|
|
18
|
+
align-items: center;
|
|
19
|
+
display: flex;
|
|
20
|
+
}
|
|
21
|
+
}
|