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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-grabber",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Youtube Grabber",
5
5
  "main": "./dist/index.js",
6
6
  "repository": {
@@ -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
- const navigateToPage = async (url: string) => {
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
- result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
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;
@@ -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.Track;
59
+ return UrlType.Other;
56
60
  };
@@ -26,3 +26,10 @@ export enum FormatScope {
26
26
  Global = "global",
27
27
  Tab = "tab"
28
28
  }
29
+
30
+ export enum InputMode {
31
+ Auto = "auto",
32
+ Artists = "artists",
33
+ Albums = "albums",
34
+ Songs = "songs",
35
+ };
@@ -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[];
@@ -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
- discographyDownload?: boolean;
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
- discographyDownload: {
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
  },
@@ -107,4 +107,5 @@ export enum UrlType {
107
107
  Artist = "artist",
108
108
  Playlist = "playlist",
109
109
  Track = "track",
110
+ Other = "other",
110
111
  };
@@ -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
+ }