yt-grabber 1.6.0 → 1.8.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.
Files changed (34) hide show
  1. package/e2e/main.spec.ts +46 -0
  2. package/eslint.config.ts +5 -4
  3. package/package.json +156 -152
  4. package/playwright.config.ts +79 -0
  5. package/src/automations/YoutubeAlbums.ts +7 -0
  6. package/src/automations/YoutubeArtists.ts +11 -7
  7. package/src/automations/YoutubeTracks.ts +7 -0
  8. package/src/common/Formatters.ts +9 -2
  9. package/src/common/Helpers.ts +21 -9
  10. package/src/common/Store.ts +12 -2
  11. package/src/common/Youtube.ts +5 -0
  12. package/src/common/YtdplUtils.ts +15 -3
  13. package/src/components/modals/failuresModal/FailuresModal.styl +22 -0
  14. package/src/components/modals/failuresModal/FailuresModal.tsx +73 -0
  15. package/src/components/modals/imageModal/ImageModal.tsx +13 -15
  16. package/src/components/numberField/NumberField.tsx +13 -3
  17. package/src/components/progress/Progress.styl +11 -0
  18. package/src/components/progress/Progress.tsx +3 -2
  19. package/src/components/youtube/formatSelector/FormatSelector.tsx +1 -0
  20. package/src/components/youtube/inputPanel/InputPanel.tsx +16 -1
  21. package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +3 -0
  22. package/src/components/youtube/trackList/TrackList.tsx +6 -1
  23. package/src/react/contexts/DataContext.tsx +2 -2
  24. package/src/resources/bin/yt-dlp.exe +0 -0
  25. package/src/resources/locales/de-DE/help.json +12 -5
  26. package/src/resources/locales/de-DE/translation.json +13 -1
  27. package/src/resources/locales/en-GB/help.json +16 -9
  28. package/src/resources/locales/en-GB/translation.json +13 -1
  29. package/src/resources/locales/pl-PL/help.json +12 -5
  30. package/src/resources/locales/pl-PL/translation.json +13 -1
  31. package/src/views/development/DevelopmentView.tsx +37 -1
  32. package/src/views/home/HomeView.tsx +74 -8
  33. package/src/views/settings/SettingsView.styl +4 -0
  34. package/src/views/settings/SettingsView.tsx +34 -2
@@ -0,0 +1,46 @@
1
+ import {findLatestBuild, parseElectronApp} from "electron-playwright-helpers";
2
+ import path from "path";
3
+ import {_electron as electron, ElectronApplication, Page} from "playwright";
4
+
5
+ import {expect, test} from "@playwright/test";
6
+
7
+ let electronApp: ElectronApplication;
8
+ let page: Page
9
+
10
+ test.beforeAll(async () => {
11
+ const latestBuild = findLatestBuild("release");
12
+ const appInfo = parseElectronApp(latestBuild);
13
+
14
+ process.env.CI = "e2e";
15
+ electronApp = await electron.launch({
16
+ args: [appInfo.main],
17
+ executablePath: path.resolve(__dirname, "..", "release", "win-unpacked", "yt-grabber.exe"),
18
+ });
19
+ electronApp.on("window", async (page) => {
20
+ const filename = page.url()?.split("/").pop();
21
+
22
+ console.log(`"Window opened: ${filename}`);
23
+ page.on("pageerror", (error) => {
24
+ console.error(error);
25
+ });
26
+ page.on("console", (msg) => {
27
+ console.log(msg.text());
28
+ });
29
+ });
30
+ });
31
+
32
+ test.afterAll(async () => {
33
+ await electronApp.close();
34
+ });
35
+
36
+ test("renders main page", async () => {
37
+ page = await electronApp.firstWindow();
38
+ await page.waitForSelector("#root");
39
+ await page.waitForSelector("h6");
40
+
41
+ const text = await page.$eval("h6", (el) => el.textContent);
42
+ expect(text).toBe("YT GRABBER");
43
+
44
+ const title = await page.title();
45
+ expect(title).toBe("Youtube Grabber");
46
+ });
package/eslint.config.ts CHANGED
@@ -47,10 +47,11 @@ export default defineConfig([{
47
47
  },
48
48
 
49
49
  rules: {
50
- indent: ["error", 4],
51
- "linebreak-style": ["error", "windows"],
52
- quotes: ["error", "double"],
53
- semi: ["error", "always"],
50
+ indent: ["warn", 4],
51
+ "linebreak-style": ["warn", "windows"],
52
+ quotes: ["warn", "double"],
53
+ semi: ["warn", "always"],
54
+ "@typescript-eslint/no-unused-vars": ["warn"],
54
55
  "@typescript-eslint/no-var-requires": 0,
55
56
  "@typescript-eslint/no-explicit-any": 0,
56
57
  "@typescript-eslint/no-empty-interface": 0,
package/package.json CHANGED
@@ -1,152 +1,156 @@
1
- {
2
- "name": "yt-grabber",
3
- "version": "1.6.0",
4
- "description": "Youtube Grabber",
5
- "main": "./dist/index.js",
6
- "repository": {
7
- "url": "https://github.com/karenpommeroy/yt-grabber.git"
8
- },
9
- "scripts": {
10
- "clean": "rimraf dist",
11
- "start": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron\"",
12
- "start:debug": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron-debug\"",
13
- "build": "yarn clean && \"cross-env NODE_ENV=development webpack\" --config webpack.config.ts",
14
- "build:prod": "yarn clean && \"cross-env NODE_ENV=production webpack\" --config webpack.config.ts",
15
- "electron": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
16
- "electron-debug": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron --inspect-brk=9229 .",
17
- "watch": "webpack --watch --config webpack.config.ts",
18
- "pack": "yarn build:prod && electron-builder --dir",
19
- "dist": "yarn build:prod && electron-builder"
20
- },
21
- "build": {
22
- "appId": "yt.grabber",
23
- "productName": "YT Grabber",
24
- "copyright": "© 2025 Marcin Karpiński",
25
- "files": [
26
- "dist/**/*",
27
- "package.json"
28
- ],
29
- "asar": true,
30
- "win": {
31
- "target": "nsis",
32
- "icon": "dist/resources/icons/logo.ico",
33
- "artifactName": "${name}-${version}-setup.${ext}"
34
- },
35
- "nsis": {
36
- "oneClick": false,
37
- "allowToChangeInstallationDirectory": true,
38
- "installerIcon": "dist/resources/icons/logo.ico",
39
- "uninstallerIcon": "dist/resources/icons/logo.ico",
40
- "installerHeaderIcon": "dist/resources/icons/logo.ico",
41
- "createDesktopShortcut": true,
42
- "createStartMenuShortcut": true,
43
- "shortcutName": "YT Grabber"
44
- },
45
- "directories": {
46
- "output": "release"
47
- },
48
- "extraResources": [
49
- {
50
- "from": "dist/resources/bin",
51
- "to": "bin",
52
- "filter": [
53
- "*.exe"
54
- ]
55
- },
56
- {
57
- "from": "dist/resources/locales",
58
- "to": "locales"
59
- },
60
- {
61
- "from": "public/profile",
62
- "to": "profile"
63
- }
64
- ]
65
- },
66
- "author": "Marcin Karpiński <mkarpins@gmail.com>",
67
- "license": "MIT",
68
- "dependencies": {
69
- "@emotion/react": "^11.14.0",
70
- "@emotion/styled": "^11.14.0",
71
- "@mui/icons-material": "^7.0.0",
72
- "@mui/lab": "^7.0.0-beta.9",
73
- "@mui/material": "^7.0.0",
74
- "axios": "^1.8.4",
75
- "classnames": "^2.5.1",
76
- "electron-devtools-installer": "^4.0.0",
77
- "electron-reload": "^2.0.0-alpha.1",
78
- "electron-store": "^8.2.0",
79
- "fs-extra": "^11.3.0",
80
- "i18next": "^24.2.2",
81
- "i18next-node-fs-backend": "^2.1.3",
82
- "jsonschema": "^1.5.0",
83
- "lodash": "^4.17.21",
84
- "mkdirp": "^3.0.1",
85
- "moment": "^2.30.1",
86
- "moment-duration-format": "^2.3.2",
87
- "mui-image": "^1.0.7",
88
- "puppeteer": "^24.4.0",
89
- "puppeteer-core": "^24.4.0",
90
- "puppeteer-extra": "^3.3.6",
91
- "puppeteer-extra-plugin-devtools": "^2.4.6",
92
- "puppeteer-extra-plugin-stealth": "^2.11.2",
93
- "react": "^19.0.0",
94
- "react-dom": "^19.0.0",
95
- "react-i18next": "^15.4.1",
96
- "react-number-format": "^5.4.3",
97
- "react-router-dom": "^7.4.0",
98
- "usehooks-ts": "^3.1.1",
99
- "yt-dlp-wrap": "^2.3.12"
100
- },
101
- "devDependencies": {
102
- "@types/eslint": "^9.6.1",
103
- "@types/fs-extra": "^11.0.4",
104
- "@types/i18next": "^13.0.0",
105
- "@types/i18next-node-fs-backend": "^2.1.5",
106
- "@types/jsonschema": "^1.1.1",
107
- "@types/lodash": "^4.17.16",
108
- "@types/moment-duration-format": "^2.2.6",
109
- "@types/mui-image": "^1.0.5",
110
- "@types/node": "^22.13.14",
111
- "@types/prettier": "^3.0.0",
112
- "@types/puppeteer-core": "^7.0.4",
113
- "@types/react": "^19.0.12",
114
- "@types/react-dom": "^19.0.4",
115
- "@types/webpack-dev-server": "^4.7.2",
116
- "@types/webpack-env": "^1.18.8",
117
- "@typescript-eslint/eslint-plugin": "^8.28.0",
118
- "@typescript-eslint/parser": "^8.28.0",
119
- "concurrently": "^9.1.2",
120
- "copy-webpack-plugin": "^13.0.0",
121
- "cross-env": "^7.0.3",
122
- "css-loader": "^7.1.2",
123
- "electron": "^35.1.2",
124
- "electron-builder": "^26.0.12",
125
- "eslint": "^9.23.0",
126
- "eslint-config-prettier": "^10.1.1",
127
- "eslint-plugin-css": "^0.11.0",
128
- "eslint-plugin-react": "^7.37.4",
129
- "eslint-webpack-plugin": "^5.0.0",
130
- "globals": "^16.0.0",
131
- "html-webpack-plugin": "^5.6.3",
132
- "i18next-scanner-webpack": "^0.9.1",
133
- "jiti": "^2.4.2",
134
- "mini-css-extract-plugin": "^2.9.2",
135
- "prettier": "^3.5.3",
136
- "raw-loader": "^4.0.2",
137
- "rimraf": "^6.0.1",
138
- "source-map-loader": "^5.0.0",
139
- "stylus": "^0.64.0",
140
- "stylus-loader": "^8.1.1",
141
- "ts-loader": "^9.5.2",
142
- "ts-node": "^10.9.2",
143
- "ts-node-dev": "^2.0.0",
144
- "typescript": "^5.8.2",
145
- "wait-on": "^8.0.2",
146
- "webpack": "^5.98.0",
147
- "webpack-cli": "^6.0.1",
148
- "webpack-dev-server": "^5.2.1",
149
- "webpack-node-externals": "^3.0.0"
150
- },
151
- "packageManager": "yarn@4.4.1"
152
- }
1
+ {
2
+ "name": "yt-grabber",
3
+ "version": "1.8.0",
4
+ "description": "Youtube Grabber",
5
+ "main": "./dist/index.js",
6
+ "repository": {
7
+ "url": "https://github.com/karenpommeroy/yt-grabber.git"
8
+ },
9
+ "scripts": {
10
+ "clean": "rimraf dist",
11
+ "start": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron\"",
12
+ "start:debug": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron-debug\"",
13
+ "build": "yarn clean && \"cross-env NODE_ENV=development webpack\" --config webpack.config.ts",
14
+ "build:prod": "yarn clean && \"cross-env NODE_ENV=production webpack\" --config webpack.config.ts",
15
+ "electron": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
16
+ "electron-debug": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron --inspect-brk=9229 .",
17
+ "watch": "webpack --watch --config webpack.config.ts",
18
+ "pack": "yarn build:prod && electron-builder --dir",
19
+ "dist": "yarn build:prod && electron-builder"
20
+ },
21
+ "build": {
22
+ "appId": "yt.grabber",
23
+ "productName": "YT Grabber",
24
+ "copyright": "© 2025 Marcin Karpiński",
25
+ "files": [
26
+ "dist/**/*",
27
+ "package.json"
28
+ ],
29
+ "publish": null,
30
+ "asar": true,
31
+ "win": {
32
+ "target": "nsis",
33
+ "icon": "dist/resources/icons/logo.ico",
34
+ "artifactName": "${name}-${version}-setup.${ext}"
35
+ },
36
+ "nsis": {
37
+ "oneClick": false,
38
+ "allowToChangeInstallationDirectory": true,
39
+ "installerIcon": "dist/resources/icons/logo.ico",
40
+ "uninstallerIcon": "dist/resources/icons/logo.ico",
41
+ "installerHeaderIcon": "dist/resources/icons/logo.ico",
42
+ "createDesktopShortcut": true,
43
+ "createStartMenuShortcut": true,
44
+ "shortcutName": "YT Grabber"
45
+ },
46
+ "directories": {
47
+ "output": "release"
48
+ },
49
+ "extraResources": [
50
+ {
51
+ "from": "dist/resources/bin",
52
+ "to": "bin",
53
+ "filter": [
54
+ "*.exe"
55
+ ]
56
+ },
57
+ {
58
+ "from": "dist/resources/locales",
59
+ "to": "locales"
60
+ },
61
+ {
62
+ "from": "public/profile",
63
+ "to": "profile"
64
+ }
65
+ ]
66
+ },
67
+ "author": "Marcin Karpiński <mkarpins@gmail.com>",
68
+ "license": "MIT",
69
+ "dependencies": {
70
+ "@emotion/react": "^11.14.0",
71
+ "@emotion/styled": "^11.14.0",
72
+ "@mui/icons-material": "^7.0.0",
73
+ "@mui/lab": "^7.0.0-beta.9",
74
+ "@mui/material": "^7.0.0",
75
+ "axios": "^1.8.4",
76
+ "classnames": "^2.5.1",
77
+ "electron-devtools-installer": "^4.0.0",
78
+ "electron-reload": "^2.0.0-alpha.1",
79
+ "electron-store": "^8.2.0",
80
+ "fs-extra": "^11.3.0",
81
+ "i18next": "^24.2.2",
82
+ "i18next-node-fs-backend": "^2.1.3",
83
+ "jsonschema": "^1.5.0",
84
+ "lodash": "^4.17.21",
85
+ "mkdirp": "^3.0.1",
86
+ "moment": "^2.30.1",
87
+ "moment-duration-format": "^2.3.2",
88
+ "puppeteer": "^24.4.0",
89
+ "puppeteer-core": "^24.4.0",
90
+ "puppeteer-extra": "^3.3.6",
91
+ "puppeteer-extra-plugin-devtools": "^2.4.6",
92
+ "puppeteer-extra-plugin-stealth": "^2.11.2",
93
+ "react": "^19.0.0",
94
+ "react-dom": "^19.0.0",
95
+ "react-i18next": "^15.4.1",
96
+ "react-number-format": "^5.4.3",
97
+ "react-router-dom": "^7.4.0",
98
+ "usehooks-ts": "^3.1.1",
99
+ "win-version-info": "^6.0.1",
100
+ "yt-dlp-wrap": "^2.3.12"
101
+ },
102
+ "devDependencies": {
103
+ "@playwright/test": "^1.53.1",
104
+ "@types/eslint": "^9.6.1",
105
+ "@types/fs-extra": "^11.0.4",
106
+ "@types/i18next": "^13.0.0",
107
+ "@types/i18next-node-fs-backend": "^2.1.5",
108
+ "@types/jsonschema": "^1.1.1",
109
+ "@types/lodash": "^4.17.16",
110
+ "@types/moment-duration-format": "^2.2.6",
111
+ "@types/mui-image": "^1.0.5",
112
+ "@types/node": "^22.13.14",
113
+ "@types/prettier": "^3.0.0",
114
+ "@types/puppeteer-core": "^7.0.4",
115
+ "@types/react": "^19.0.12",
116
+ "@types/react-dom": "^19.0.4",
117
+ "@types/webpack-dev-server": "^4.7.2",
118
+ "@types/webpack-env": "^1.18.8",
119
+ "@types/win-version-info": "^3.1.3",
120
+ "@typescript-eslint/eslint-plugin": "^8.28.0",
121
+ "@typescript-eslint/parser": "^8.28.0",
122
+ "concurrently": "^9.1.2",
123
+ "copy-webpack-plugin": "^13.0.0",
124
+ "cross-env": "^7.0.3",
125
+ "css-loader": "^7.1.2",
126
+ "electron": "^36.5.0",
127
+ "electron-builder": "26.0.16",
128
+ "electron-playwright-helpers": "^1.7.1",
129
+ "eslint": "^9.23.0",
130
+ "eslint-config-prettier": "^10.1.1",
131
+ "eslint-plugin-css": "^0.11.0",
132
+ "eslint-plugin-react": "^7.37.4",
133
+ "eslint-webpack-plugin": "^5.0.0",
134
+ "globals": "^16.0.0",
135
+ "html-webpack-plugin": "^5.6.3",
136
+ "i18next-scanner-webpack": "^0.9.1",
137
+ "jiti": "^2.4.2",
138
+ "mini-css-extract-plugin": "^2.9.2",
139
+ "prettier": "^3.5.3",
140
+ "raw-loader": "^4.0.2",
141
+ "rimraf": "^6.0.1",
142
+ "source-map-loader": "^5.0.0",
143
+ "stylus": "^0.64.0",
144
+ "stylus-loader": "^8.1.1",
145
+ "ts-loader": "^9.5.2",
146
+ "ts-node": "^10.9.2",
147
+ "ts-node-dev": "^2.0.0",
148
+ "typescript": "^5.8.2",
149
+ "wait-on": "^8.0.2",
150
+ "webpack": "^5.98.0",
151
+ "webpack-cli": "^6.0.1",
152
+ "webpack-dev-server": "^5.2.1",
153
+ "webpack-node-externals": "^3.0.0"
154
+ },
155
+ "packageManager": "yarn@4.4.1"
156
+ }
@@ -0,0 +1,79 @@
1
+ import {defineConfig, devices} from "@playwright/test";
2
+
3
+ /**
4
+ * Read environment variables from file.
5
+ * https://github.com/motdotla/dotenv
6
+ */
7
+ // import dotenv from "dotenv";
8
+ // import path from "path";
9
+ // dotenv.config({ path: path.resolve(__dirname, ".env") });
10
+
11
+ /**
12
+ * See https://playwright.dev/docs/test-configuration.
13
+ */
14
+ export default defineConfig({
15
+ testDir: "./e2e",
16
+ /* Run tests in files in parallel */
17
+ fullyParallel: true,
18
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
19
+ forbidOnly: !!process.env.CI,
20
+ /* Retry on CI only */
21
+ retries: process.env.CI ? 2 : 0,
22
+ /* Opt out of parallel tests on CI. */
23
+ workers: process.env.CI ? 1 : undefined,
24
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
25
+ reporter: "html",
26
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
27
+ use: {
28
+ /* Base URL to use in actions like `await page.goto("/")`. */
29
+ // baseURL: "http://localhost:3000",
30
+
31
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
32
+ trace: "on-first-retry",
33
+ },
34
+
35
+ /* Configure projects for major browsers */
36
+ projects: [
37
+ {
38
+ name: "chromium",
39
+ use: {...devices["Desktop Chrome"]},
40
+ },
41
+
42
+ // {
43
+ // name: "firefox",
44
+ // use: { ...devices["Desktop Firefox"] },
45
+ // },
46
+
47
+ // {
48
+ // name: "webkit",
49
+ // use: { ...devices["Desktop Safari"] },
50
+ // },
51
+
52
+ /* Test against mobile viewports. */
53
+ // {
54
+ // name: "Mobile Chrome",
55
+ // use: { ...devices["Pixel 5"] },
56
+ // },
57
+ // {
58
+ // name: "Mobile Safari",
59
+ // use: { ...devices["iPhone 12"] },
60
+ // },
61
+
62
+ /* Test against branded browsers. */
63
+ // {
64
+ // name: "Microsoft Edge",
65
+ // use: { ...devices["Desktop Edge"], channel: "msedge" },
66
+ // },
67
+ // {
68
+ // name: "Google Chrome",
69
+ // use: { ...devices["Desktop Chrome"], channel: "chrome" },
70
+ // },
71
+ ],
72
+
73
+ /* Run your local dev server before starting the tests */
74
+ // webServer: {
75
+ // command: "npm run start",
76
+ // url: "http://localhost:3000",
77
+ // reuseExistingServer: !process.env.CI,
78
+ // },
79
+ });
@@ -71,8 +71,15 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
71
71
 
72
72
  const process = async (album: string) => {
73
73
  const results: string[] = [];
74
+ const albumUrlRegex = /^https?:\/\/.*playlist/i;
74
75
 
75
76
  try {
77
+ if (albumUrlRegex.test(album)) {
78
+ results.push(album);
79
+
80
+ return results;
81
+ }
82
+
76
83
  const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
77
84
  await clearInput(searchInput, page);
78
85
  await searchInput.type(album);
@@ -85,13 +85,6 @@ const run = async (
85
85
 
86
86
  const process = async (artist: string) => {
87
87
  const results: string[] = [];
88
- const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
89
-
90
- await clearInput(searchInput, page);
91
- await searchInput.type(artist);
92
- page.keyboard.press("Enter");
93
- await page.waitForNetworkIdle();
94
-
95
88
  const artistChannelUrl = await getArtistUrl(params, artist, onPause);
96
89
 
97
90
  await navigateToPage(artistChannelUrl, page);
@@ -120,6 +113,17 @@ const run = async (
120
113
 
121
114
  const getArtistUrl = async (params: GetYoutubeParams, artist: string, onPause?: (data: YoutubeArtist[]) => Promise<YoutubeArtist>): Promise<string> => {
122
115
  try {
116
+ const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
117
+ const channelUrlRegex = /^https?:\/\/.*channel/i;
118
+
119
+ if (channelUrlRegex.test(artist)) {
120
+ return artist;
121
+ } else {
122
+ await clearInput(searchInput, page);
123
+ await searchInput.type(artist);
124
+ page.keyboard.press("Enter");
125
+ await page.waitForNetworkIdle();
126
+ }
123
127
  const artistsChip = await page.waitForSelector(`::-p-xpath(${YtMusicArtistsChipSelector})`, {visible: true, timeout: 1000});
124
128
 
125
129
  artistsChip.click();
@@ -72,8 +72,15 @@ const run = async (params: GetYoutubeParams, options: LaunchOptions, i18n: i18ne
72
72
 
73
73
  const process = async (song: string) => {
74
74
  const results: string[] = [];
75
+ const trackUrlRegex = /^https?:\/\/.*watch/i;
75
76
 
76
77
  try {
78
+ if (trackUrlRegex.test(song)) {
79
+ results.push(song);
80
+
81
+ return results;
82
+ }
83
+
77
84
  const searchInput = await page.waitForSelector(`::-p-xpath(${YtMusicSearchInputSelector})`, {timeout: 1000});
78
85
  await clearInput(searchInput, page);
79
86
  await searchInput.type(song);
@@ -1,6 +1,7 @@
1
1
  import _find from "lodash/find";
2
2
  import _first from "lodash/first";
3
3
  import _get from "lodash/get";
4
+ import _last from "lodash/last";
4
5
  import _sumBy from "lodash/sumBy";
5
6
 
6
7
  import {AlbumInfo, TrackInfo} from "./Youtube";
@@ -10,16 +11,22 @@ export const getAlbumInfo = (items: TrackInfo[], url?: string): AlbumInfo => {
10
11
 
11
12
  return {
12
13
  id: item.playlist_id ?? item.id,
13
- artist: _get(item, "creators.0", _get(item, "artist", item.channel)),
14
+ artist: isPlaylistTrack(item) ? _get(item, "uploader", _get(item, "channel", _get(item, "playlist_uploader"))) : _get(item, "creators.0", _get(item, "artist", item.channel)),
14
15
  title: isAlbumTrack(item) ? _get(item, "album", _get(item, "playlist_title", _get(item, "playlist"))) : item.title,
15
16
  releaseYear: _get(item, "release_year") ?? (new Date(item.timestamp * 1000)).getFullYear(),
16
17
  tracksNumber: _get(item, "playlist_count", 1),
17
18
  duration: _sumBy(items, "duration"),
18
- thumbnail: _get(item, "thumbnail", _get(_find(item.thumbnails, ["id", "2"]), "url")),
19
+ thumbnail: _get(item, "thumbnail", _get(_find(item.thumbnails, ["id", "2"]) ?? _last(item.thumbnails), "url")),
19
20
  url,
20
21
  };
21
22
  };
22
23
 
24
+ export const isPlaylistTrack = (track: TrackInfo) => {
25
+ const appOptions = global.store.get("application");
26
+
27
+ return track.playlist_count > appOptions.playlistCountThreshold;
28
+ };
29
+
23
30
  export const isAlbumTrack = (track: TrackInfo) => {
24
31
  return !!track.playlist;
25
32
  };
@@ -1,6 +1,7 @@
1
1
  import _groupBy from "lodash/groupBy";
2
2
  import _includes from "lodash/includes";
3
3
  import _indexOf from "lodash/indexOf";
4
+ import _isNumber from "lodash/isNumber";
4
5
  import _keys from "lodash/keys";
5
6
  import _map from "lodash/map";
6
7
  import _replace from "lodash/replace";
@@ -11,6 +12,7 @@ import {TrackInfo, UrlType, YoutubeInfoResult} from "./Youtube";
11
12
  export const isDev = () => process.env.NODE_ENV === "development";
12
13
 
13
14
  export const formatFileSize = (sizeInBytes: number, decimals = 2) => {
15
+ if (!_isNumber(sizeInBytes)) return "";
14
16
  if (sizeInBytes === 0) return "0 Bytes";
15
17
 
16
18
  const k = 1024;
@@ -41,20 +43,30 @@ export const resolveMockData = (delay = 1000) => {
41
43
 
42
44
  export const waitFor = (miliseconds: number) => new Promise((resolve) => setTimeout(resolve, miliseconds));
43
45
 
44
- export const getUrlType = (url: string) => {
45
- const artistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/|youtu\.be\/)?(channel)/;
46
+ export const isPlaylist = (url: string) => {
46
47
  const playlistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/(?:playlist\?list=|watch\?.*?\blist=)([a-zA-Z0-9_-]+)/;
48
+
49
+ return playlistRegex.test(url);
50
+ };
51
+
52
+ export const isArtist = (url: string) => {
53
+ const artistRegex = /^(?:https?:\/\/)?(?:www\.)?(?:m\.)?(?:music\.)?(?:youtube\.com\/|youtu\.be\/)?(channel)/;
54
+
55
+ return artistRegex.test(url);
56
+ };
57
+
58
+ export const isTrack = (url: string) => {
47
59
  const trackRegex = /^(?:https?:\/\/)?(?:www\.)?(?:music\.)?youtube\.com\/watch\?.*?\bv=([a-zA-Z0-9_-]+)/;
48
60
 
49
- const isArtist = artistRegex.test(url);
50
- const isPlaylist = playlistRegex.test(url);
51
- const isTrack = trackRegex.test(url);
52
-
53
- if (isArtist) {
61
+ return trackRegex.test(url);
62
+ };
63
+
64
+ export const getUrlType = (url: string) => {
65
+ if (isArtist(url)) {
54
66
  return UrlType.Artist;
55
- } else if (isPlaylist) {
67
+ } else if (isPlaylist(url)) {
56
68
  return UrlType.Playlist;
57
- } else if (isTrack) {
69
+ } else if (isTrack(url)) {
58
70
  return UrlType.Track;
59
71
  }
60
72
 
@@ -14,6 +14,8 @@ export type ApplicationOptions = {
14
14
  trackOutputTemplate?: string;
15
15
  concurrency?: number;
16
16
  quality?: number;
17
+ playlistCountThreshold?: number;
18
+ playlistCheckMaxItemsCount?: number;
17
19
  debugMode?: boolean;
18
20
  formatScope?: FormatScope;
19
21
  multiMatchAction?: MultiMatchAction;
@@ -88,11 +90,19 @@ export const StoreSchema: Schema<IStore> = {
88
90
  },
89
91
  concurrency: {
90
92
  type: "integer",
91
- default: 3
93
+ default: 5
92
94
  },
93
95
  quality: {
94
96
  type: "integer",
95
- default: 0
97
+ default: 10
98
+ },
99
+ playlistCountThreshold: {
100
+ type: "integer",
101
+ default: 25
102
+ },
103
+ playlistCheckMaxItemsCount: {
104
+ type: "integer",
105
+ default: 3
96
106
  },
97
107
  debugMode: {
98
108
  type: "boolean",