yt-grabber 1.0.0 → 1.2.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 (98) hide show
  1. package/.yarnrc.yml +3 -1
  2. package/README.md +99 -8
  3. package/eslint.config.ts +60 -0
  4. package/package.json +67 -32
  5. package/public/banner.png +0 -0
  6. package/public/profile/cookies.json +18 -0
  7. package/public/screenshots/main-downloading.png +0 -0
  8. package/public/screenshots/main-info.png +0 -0
  9. package/public/screenshots/main.png +0 -0
  10. package/public/screenshots/settings.png +0 -0
  11. package/src/App.styl +49 -19
  12. package/src/App.tsx +31 -4
  13. package/src/automations/Selectors.ts +7 -0
  14. package/src/automations/Youtube.ts +138 -0
  15. package/src/common/FileSystem.ts +5 -168
  16. package/src/common/Formatters.ts +25 -0
  17. package/src/common/Helpers.ts +40 -254
  18. package/src/common/Media.ts +28 -0
  19. package/src/common/Messaging.ts +38 -0
  20. package/src/common/Promise.ts +14 -0
  21. package/src/common/PuppeteerOptions.ts +3 -2
  22. package/src/common/Reporter.ts +61 -0
  23. package/src/common/Store.ts +26 -26
  24. package/src/common/Youtube.ts +35 -5
  25. package/src/common/YtdplUtils.ts +158 -0
  26. package/src/components/appBar/AppBar.styl +42 -17
  27. package/src/components/appBar/AppBar.tsx +38 -12
  28. package/src/components/fileField/FileField.tsx +25 -81
  29. package/src/components/languagePicker/LanguagePicker.styl +36 -36
  30. package/src/components/languagePicker/LanguagePicker.tsx +11 -11
  31. package/src/components/modals/{DetailsModal.styl → detailsModal/DetailsModal.styl} +1 -3
  32. package/src/components/modals/{DetailsModal.tsx → detailsModal/DetailsModal.tsx} +3 -7
  33. package/src/components/modals/imageModal/ImageModal.styl +25 -0
  34. package/src/components/modals/imageModal/ImageModal.tsx +72 -0
  35. package/src/components/numberField/NumberField.styl +14 -12
  36. package/src/components/progress/Progress.tsx +15 -6
  37. package/src/components/themePicker/ThemePicker.styl +19 -19
  38. package/src/components/themePicker/ThemePicker.tsx +11 -1
  39. package/src/components/youtube/formatSelector/FormatSelector.tsx +61 -43
  40. package/src/components/youtube/infoBar/InfoBar.styl +41 -0
  41. package/src/components/youtube/infoBar/InfoBar.tsx +83 -0
  42. package/src/components/youtube/inputPanel/InputPanel.styl +13 -1
  43. package/src/components/youtube/inputPanel/InputPanel.tsx +146 -116
  44. package/src/components/youtube/logMenu/LogMenu.styl +23 -0
  45. package/src/components/youtube/logMenu/LogMenu.tsx +135 -0
  46. package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.styl +90 -80
  47. package/src/components/youtube/mediaInfoPanel/MediaInfoPanel.tsx +100 -19
  48. package/src/components/youtube/playlistTabs/PlaylistTabs.styl +67 -0
  49. package/src/components/youtube/playlistTabs/PlaylistTabs.tsx +238 -0
  50. package/src/components/youtube/trackList/TrackList.styl +11 -7
  51. package/src/components/youtube/trackList/TrackList.tsx +114 -39
  52. package/src/hooks/useHelp.ts +153 -0
  53. package/src/hooks/useWindowUpdater.ts +24 -0
  54. package/src/i18next.ts +23 -4
  55. package/src/index.ts +99 -6
  56. package/src/react/actions/AppActions.ts +3 -16
  57. package/src/react/contexts/AppContext.tsx +1 -6
  58. package/src/react/contexts/DataContext.tsx +90 -15
  59. package/src/react/reducers/AppReducer.tsx +2 -18
  60. package/src/react/states/AppState.ts +2 -11
  61. package/src/resources/bin/yt-dlp.exe +0 -0
  62. package/src/resources/icons/logo.ico +0 -0
  63. package/src/resources/locales/de-DE/help.json +134 -0
  64. package/src/resources/locales/de-DE/translation.json +36 -7
  65. package/src/resources/locales/en-GB/help.json +134 -0
  66. package/src/resources/locales/en-GB/translation.json +34 -5
  67. package/src/resources/locales/pl-PL/help.json +134 -0
  68. package/src/resources/locales/pl-PL/translation.json +37 -8
  69. package/src/styles/MaterialThemes.ts +1 -3
  70. package/src/views/development/DevelopmentView.styl +5 -0
  71. package/src/views/development/DevelopmentView.tsx +32 -3
  72. package/src/views/home/HomeView.styl +12 -4
  73. package/src/views/home/HomeView.tsx +387 -340
  74. package/src/views/settings/SettingsView.styl +10 -0
  75. package/src/views/settings/SettingsView.tsx +54 -22
  76. package/tests/TestMultipleMock.ts +91779 -0
  77. package/webpack.config.ts +14 -10
  78. package/public/screenshots/cutting.png +0 -0
  79. package/public/screenshots/downloading.png +0 -0
  80. package/public/screenshots/editing.png +0 -0
  81. package/public/screenshots/errors.png +0 -0
  82. package/public/screenshots/tracklist.png +0 -0
  83. package/src/common/Mappings.ts +0 -14
  84. package/src/common/Selectors.ts +0 -21
  85. package/src/components/directoryPicker/DirectoryPicker.tsx +0 -44
  86. package/src/components/splitButton/SplitButton.styl +0 -0
  87. package/src/components/splitButton/SplitButton.tsx +0 -125
  88. package/src/components/themeSwitcher/ThemeSwitcher.styl +0 -10
  89. package/src/components/themeSwitcher/ThemeSwitcher.tsx +0 -43
  90. package/src/enums/DataResponse.ts +0 -5
  91. package/src/enums/Media.ts +0 -16
  92. package/src/enums/MediaFormat.ts +0 -10
  93. package/src/enums/MimeTypes.ts +0 -14
  94. package/src/hooks/useData.ts +0 -61
  95. package/src/react/contexts/DataContext copy.tsx +0 -76
  96. package/src/resources/images/logo.png +0 -0
  97. package/src/tests/MissingDetailsTracksMock.ts +0 -7737
  98. /package/{src/tests/CompleteTracksMock.ts → tests/TestPlaylistMock.ts} +0 -0
package/.yarnrc.yml CHANGED
@@ -10,4 +10,6 @@ packageExtensions:
10
10
  "react": "^19.0.0"
11
11
  peerDependencies:
12
12
  "react": "^19.0.0"
13
-
13
+ "eslint@*":
14
+ dependencies:
15
+ "globals": "^16.0.0"
package/README.md CHANGED
@@ -1,11 +1,102 @@
1
- ## Install:
2
- `yarn install`
1
+ <img src="public/banner.png" alt="YT Grabber Banner" width="800">
3
2
 
4
- ## Build:
5
- `yarn build`
3
+ ---
6
4
 
7
- ## Start:
8
- `yarn start`
5
+ **YT Grabber** is a robust desktop application designed to retrieve multimedia from YouTube and YouTube Music services.
9
6
 
10
- ## Usage:
11
- Download and run latest binary release.
7
+ It provides responsive UI to manage your downloads and automation features improve and accelerate download process.
8
+
9
+ It provides support for downloading:
10
+
11
+ - videos
12
+ - audio tracks
13
+ - complete playlists
14
+ - full discographies
15
+
16
+ Various formats and quality options are available for both - audio and video.
17
+
18
+ Each download can be customized to your needs for easy workflow automation.
19
+
20
+ ## Table of Contents
21
+
22
+ - [Table of Contents](#table-of-contents)
23
+ - [Features](#features)
24
+ - [Screenshots](#screenshots)
25
+ - [Usage](#usage)
26
+ - [Development](#development)
27
+ - [Running](#running)
28
+ - [Packaging](#packaging)
29
+ - [License](#license)
30
+ - [Legal Disclaimer](#legal-disclaimer)
31
+
32
+
33
+ ## Features
34
+
35
+ * Download video, audio, playlists and complete artist discographies
36
+
37
+ * Multiple output formats (mp3, m4u, flac, wav, mp4, mkv)
38
+
39
+ * Customizable audio and video quality
40
+
41
+ * Trimming video and audio
42
+
43
+ * Batch multimedia download
44
+
45
+ * Metadata (tags) embedding
46
+
47
+ * Configurable output
48
+
49
+ * Multiple language support (English, German, Polish out of the box)
50
+
51
+ * Light/Dark theme
52
+
53
+ * Responsive and clean UI
54
+
55
+
56
+ ## Screenshots
57
+
58
+ <img src="public/screenshots/main.png" alt="Main" width="600">
59
+
60
+ *Main window*
61
+
62
+ <img src="public/screenshots/main-info.png" alt="Main: Loading Info" width="600">
63
+
64
+ *Displaying multimedia info*
65
+
66
+ <img src="public/screenshots/main-downloading.png" alt="Main: Downloading Files" width="600">
67
+
68
+ *Downloading multimedia*
69
+
70
+ <img src="public/screenshots/settings.png" alt="Settings" width="600">
71
+
72
+ *Settings*
73
+
74
+ ## Usage
75
+
76
+ Download and run latest release installer from [here](https://github.com/karenpommeroy/yt-grabber/releases).
77
+
78
+ ## Development
79
+
80
+ To build **yt-grabber** follow these steps:
81
+
82
+ 1. Clone this repository
83
+ 2. Install dependencies using `npm install` or `yarn install` command
84
+ 3. If using `yarn` with Visual Studio Code also run `yarn dlx @yarnpkg/sdks vscode`
85
+ 4. Run `npm build` or `yarn build` command to build
86
+
87
+ ## Running
88
+
89
+ Run `npm start` or `yarn start` to run the app for development.
90
+ This will start webpack development server that will watch for changes to source code and reload the application automatically.
91
+
92
+ ## Packaging
93
+
94
+ To prepare release application package run `npm dist` or `yarn dist` command.
95
+
96
+ ## License
97
+
98
+ This project is licensed under the [MIT License](LICENSE).
99
+
100
+ ## Legal Disclaimer
101
+
102
+ All music files downloaded through this software must be legally owned and purchased by the user. By downloading music via this software, you represent that you have purchased and fully own the rights to any downloaded content or an active subscription to YouTube Music. Downloading or distributing pirated or illegal music copies is strictly prohibited. I claim no ownership rights to any downloaded music files - all such rights remain with the content owner. I accept no liability for the illegal use of any files downloaded through this software.
@@ -0,0 +1,60 @@
1
+ import react from "eslint-plugin-react";
2
+ import {defineConfig} from "eslint/config";
3
+ import globals from "globals";
4
+ import path from "node:path";
5
+ import {fileURLToPath} from "node:url";
6
+
7
+ import {FlatCompat} from "@eslint/eslintrc";
8
+ import js from "@eslint/js";
9
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
10
+ import tsParser from "@typescript-eslint/parser";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const compat = new FlatCompat({
15
+ baseDirectory: __dirname,
16
+ recommendedConfig: js.configs.recommended,
17
+ allConfig: js.configs.all
18
+ });
19
+
20
+ export default defineConfig([{
21
+ extends: compat.extends(
22
+ "eslint:recommended",
23
+ "plugin:react/recommended",
24
+ "plugin:@typescript-eslint/recommended",
25
+ "prettier",
26
+ ),
27
+
28
+ plugins: {
29
+ react,
30
+ "@typescript-eslint": typescriptEslint as any,
31
+ },
32
+
33
+ languageOptions: {
34
+ globals: {
35
+ ...globals.browser,
36
+ },
37
+
38
+ parser: tsParser,
39
+ ecmaVersion: "latest",
40
+ sourceType: "module",
41
+ },
42
+
43
+ settings: {
44
+ react: {
45
+ version: "detect",
46
+ },
47
+ },
48
+
49
+ rules: {
50
+ indent: ["error", 4],
51
+ "linebreak-style": ["error", "windows"],
52
+ quotes: ["error", "double"],
53
+ semi: ["error", "always"],
54
+ "@typescript-eslint/no-var-requires": 0,
55
+ "@typescript-eslint/no-explicit-any": 0,
56
+ "@typescript-eslint/no-empty-interface": 0,
57
+ "react/prop-types": 0,
58
+ "react/display-name": 0,
59
+ },
60
+ }]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-grabber",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Youtube Grabber",
5
5
  "main": "./dist/index.js",
6
6
  "repository": {
@@ -9,28 +9,56 @@
9
9
  "scripts": {
10
10
  "clean": "rimraf dist",
11
11
  "start": "concurrently \"cross-env NODE_ENV=development yarn run watch\" \"yarn run electron\"",
12
- "build": "cross-env NODE_ENV=development && yarn clean && webpack --config webpack.config.ts",
13
- "build:prod": "cross-env NODE_ENV=production && yarn clean && webpack --config webpack.config.ts",
14
- "electron": "wait-on dist/index.js && cross-env NODE_ENV=production ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
12
+ "build": "yarn clean && \"cross-env NODE_ENV=development webpack\" --config webpack.config.ts",
13
+ "build:prod": "yarn clean && \"cross-env NODE_ENV=production webpack\" --config webpack.config.ts",
14
+ "electron": "wait-on dist/index.js && ELECTRON_DISABLE_SECURITY_WARNINGS=true electron .",
15
15
  "watch": "webpack --watch --config webpack.config.ts",
16
16
  "pack": "yarn build:prod && electron-builder --dir",
17
17
  "dist": "yarn build:prod && electron-builder"
18
18
  },
19
19
  "build": {
20
20
  "appId": "yt.grabber",
21
+ "productName": "YT Grabber",
22
+ "copyright": "© 2025 Marcin Karpiński",
21
23
  "files": [
22
24
  "dist/**/*",
23
25
  "package.json"
24
26
  ],
27
+ "asar": true,
28
+ "win": {
29
+ "target": "nsis",
30
+ "icon": "dist/resources/icons/logo.ico",
31
+ "artifactName": "${productName}-setup-${version}.${ext}"
32
+ },
33
+ "nsis": {
34
+ "oneClick": false,
35
+ "allowToChangeInstallationDirectory": true,
36
+ "installerIcon": "dist/resources/icons/logo.ico",
37
+ "uninstallerIcon": "dist/resources/icons/logo.ico",
38
+ "installerHeaderIcon": "dist/resources/icons/logo.ico",
39
+ "createDesktopShortcut": true,
40
+ "createStartMenuShortcut": true,
41
+ "shortcutName": "YT Grabber"
42
+ },
25
43
  "directories": {
26
44
  "output": "release"
27
45
  },
28
46
  "extraResources": [
29
- {
30
- "from": "dist/resources/bin",
31
- "to": "bin",
32
- "filter": ["*.exe"]
33
- }
47
+ {
48
+ "from": "dist/resources/bin",
49
+ "to": "bin",
50
+ "filter": [
51
+ "*.exe"
52
+ ]
53
+ },
54
+ {
55
+ "from": "dist/resources/locales",
56
+ "to": "locales"
57
+ },
58
+ {
59
+ "from": "public/profile",
60
+ "to": "profile"
61
+ }
34
62
  ]
35
63
  },
36
64
  "author": "Marcin Karpiński <mkarpins@gmail.com>",
@@ -38,10 +66,13 @@
38
66
  "dependencies": {
39
67
  "@emotion/react": "^11.14.0",
40
68
  "@emotion/styled": "^11.14.0",
41
- "@mui/icons-material": "^6.4.2",
42
- "@mui/material": "^6.4.2",
69
+ "@mui/icons-material": "^7.0.0",
70
+ "@mui/lab": "^7.0.0-beta.9",
71
+ "@mui/material": "^7.0.0",
72
+ "axios": "^1.8.4",
43
73
  "classnames": "^2.5.1",
44
74
  "electron-devtools-installer": "^4.0.0",
75
+ "electron-reload": "^2.0.0-alpha.1",
45
76
  "electron-store": "^8.2.0",
46
77
  "fs-extra": "^11.3.0",
47
78
  "i18next": "^24.2.2",
@@ -51,17 +82,18 @@
51
82
  "mkdirp": "^3.0.1",
52
83
  "moment": "^2.30.1",
53
84
  "moment-duration-format": "^2.3.2",
54
- "puppeteer": "^24.1.1",
55
- "puppeteer-core": "^24.1.1",
85
+ "mui-image": "^1.0.7",
86
+ "puppeteer": "^24.4.0",
87
+ "puppeteer-core": "^24.4.0",
56
88
  "puppeteer-extra": "^3.3.6",
57
89
  "puppeteer-extra-plugin-devtools": "^2.4.6",
58
90
  "puppeteer-extra-plugin-stealth": "^2.11.2",
59
91
  "react": "^19.0.0",
60
92
  "react-dom": "^19.0.0",
61
- "react-i18next": "^15.4.0",
93
+ "react-i18next": "^15.4.1",
62
94
  "react-number-format": "^5.4.3",
63
- "react-router-dom": "^7.1.5",
64
- "usehooks-ts": "^3.1.0",
95
+ "react-router-dom": "^7.4.0",
96
+ "usehooks-ts": "^3.1.1",
65
97
  "yt-dlp-wrap": "^2.3.12"
66
98
  },
67
99
  "devDependencies": {
@@ -70,32 +102,35 @@
70
102
  "@types/i18next": "^13.0.0",
71
103
  "@types/i18next-node-fs-backend": "^2.1.5",
72
104
  "@types/jsonschema": "^1.1.1",
73
- "@types/lodash": "^4.17.15",
105
+ "@types/lodash": "^4.17.16",
74
106
  "@types/moment-duration-format": "^2.2.6",
75
- "@types/node": "^22.13.1",
107
+ "@types/mui-image": "^1.0.5",
108
+ "@types/node": "^22.13.14",
76
109
  "@types/prettier": "^3.0.0",
77
110
  "@types/puppeteer-core": "^7.0.4",
78
- "@types/react": "^19.0.8",
79
- "@types/react-dom": "^19.0.3",
111
+ "@types/react": "^19.0.12",
112
+ "@types/react-dom": "^19.0.4",
80
113
  "@types/webpack-dev-server": "^4.7.2",
81
114
  "@types/webpack-env": "^1.18.8",
82
- "@typescript-eslint/eslint-plugin": "^8.23.0",
83
- "@typescript-eslint/parser": "^8.23.0",
115
+ "@typescript-eslint/eslint-plugin": "^8.28.0",
116
+ "@typescript-eslint/parser": "^8.28.0",
84
117
  "concurrently": "^9.1.2",
85
- "copy-webpack-plugin": "^12.0.2",
118
+ "copy-webpack-plugin": "^13.0.0",
86
119
  "cross-env": "^7.0.3",
87
120
  "css-loader": "^7.1.2",
88
- "electron": "^34.0.2",
89
- "electron-builder": "^25.1.8",
90
- "eslint": "^9.19.0",
91
- "eslint-config-prettier": "^10.0.1",
121
+ "electron": "^35.1.2",
122
+ "electron-builder": "^26.0.12",
123
+ "eslint": "^9.23.0",
124
+ "eslint-config-prettier": "^10.1.1",
92
125
  "eslint-plugin-css": "^0.11.0",
93
126
  "eslint-plugin-react": "^7.37.4",
94
- "eslint-webpack-plugin": "^4.2.0",
127
+ "eslint-webpack-plugin": "^5.0.0",
128
+ "globals": "^16.0.0",
95
129
  "html-webpack-plugin": "^5.6.3",
96
130
  "i18next-scanner-webpack": "^0.9.1",
131
+ "jiti": "^2.4.2",
97
132
  "mini-css-extract-plugin": "^2.9.2",
98
- "prettier": "^3.4.2",
133
+ "prettier": "^3.5.3",
99
134
  "raw-loader": "^4.0.2",
100
135
  "rimraf": "^6.0.1",
101
136
  "source-map-loader": "^5.0.0",
@@ -104,11 +139,11 @@
104
139
  "ts-loader": "^9.5.2",
105
140
  "ts-node": "^10.9.2",
106
141
  "ts-node-dev": "^2.0.0",
107
- "typescript": "^5.7.3",
142
+ "typescript": "^5.8.2",
108
143
  "wait-on": "^8.0.2",
109
- "webpack": "^5.97.1",
144
+ "webpack": "^5.98.0",
110
145
  "webpack-cli": "^6.0.1",
111
- "webpack-dev-server": "^5.2.0",
146
+ "webpack-dev-server": "^5.2.1",
112
147
  "webpack-node-externals": "^3.0.0"
113
148
  },
114
149
  "packageManager": "yarn@4.4.1"
Binary file
@@ -0,0 +1,18 @@
1
+ [
2
+ {
3
+ "name": "SOCS",
4
+ "value": "CAISNQgREitib3FfaWRlbnRpdHlmcm9udGVuZHVpc2VydmVyXzIwMjUwMzI2LjA4X3AwGgJwbCACGgYIgKqSvwY",
5
+ "domain": ".youtube.com",
6
+ "path": "/",
7
+ "expires": 2298668700.829343,
8
+ "size": 91,
9
+ "httpOnly": false,
10
+ "secure": true,
11
+ "session": false,
12
+ "sameSite": "Lax",
13
+ "priority": "Medium",
14
+ "sameParty": false,
15
+ "sourceScheme": "Secure",
16
+ "sourcePort": 443
17
+ }
18
+ ]
Binary file
Binary file
Binary file
package/src/App.styl CHANGED
@@ -1,24 +1,54 @@
1
1
  @import "./styles/mixins.styl"
2
2
  @import "./styles/fonts.styl"
3
3
 
4
- html
5
- font-size: 1rem
6
- font-family: "Lato"
4
+ html {
5
+ font-size: 1rem;
6
+ font-family: "Lato";
7
7
 
8
- body
9
- margin: 0
10
- padding: 0
11
- no-select()
8
+ body {
9
+ margin: 0;
10
+ padding: 0;
11
+ no-select();
12
+
13
+ :global(#root) {
14
+ position: absolute;
15
+ width: 100%;
16
+ height: 100%;
17
+ overflow: hidden;
18
+ }
12
19
 
13
- :global(#root)
14
- position: absolute
15
- width: 100%
16
- height: 100%
17
- overflow: hidden
18
-
19
- .app
20
- background-color: var(--theme-palette-background-paper);
21
- display: flex
22
- flex-direction: column
23
- height: 100vh
24
- width: 100vw
20
+ .app {
21
+ background-color: var(--theme-palette-background-paper);
22
+ display: flex;
23
+ flex-direction: column;
24
+ height: 100vh;
25
+ width: 100vw;
26
+
27
+ &.help {
28
+ cursor: help !important;
29
+ }
30
+ }
31
+
32
+ .help-popup {
33
+ z-index: 9999999;
34
+ padding: 12px;
35
+
36
+ .header {
37
+ font-weight: bold;
38
+ margin-bottom: 8px;
39
+ }
40
+
41
+ .content {
42
+ p {
43
+ &:first-child {
44
+ margin-top: 0;
45
+ }
46
+
47
+ &:last-child {
48
+ margin-bottom: 0;
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
package/src/App.tsx CHANGED
@@ -1,28 +1,55 @@
1
+ import classnames from "classnames";
2
+ import _isArray from "lodash/isArray";
3
+ import _map from "lodash/map";
4
+ import _split from "lodash/split";
1
5
  import React from "react";
2
6
  import {HashRouter, Route, Routes} from "react-router-dom";
3
7
 
4
- import {Box, CssBaseline} from "@mui/material";
8
+ import {Box, CssBaseline, Paper, Popper, Typography} from "@mui/material";
5
9
 
6
10
  import Styles from "./App.styl";
7
11
  import AppBar from "./components/appBar/AppBar";
12
+ import useHelp from "./hooks/useHelp";
8
13
  import {useAppContext} from "./react/contexts/AppContext";
9
14
  import DevelopmentView from "./views/development/DevelopmentView";
10
15
  import {HomeView} from "./views/home/HomeView";
11
16
  import SettingsView from "./views/settings/SettingsView";
12
17
 
13
18
  export const App: React.FC = () => {
14
- const { state } = useAppContext();
19
+ const {state} = useAppContext();
20
+ const {anchorEl, help} = useHelp();
21
+
22
+ const renderLineBreaks = (value: string) => {
23
+ return _map(_split(value, "\n"), (v, k) => <React.Fragment key={k}>{v}<br /></React.Fragment>);
24
+ };
25
+
26
+ const renderText = (value: string | string[]) => {
27
+ const valueArray = _isArray(value) ? value : [value];
28
+
29
+ return _map(valueArray, (v, k) => <p key={k}>{renderLineBreaks(v)}</p>);
30
+ };
15
31
 
16
32
  return (
17
33
  <HashRouter>
18
- <Box className={Styles.app}>
34
+ <Box className={classnames(Styles.app, {[Styles.help]: state.help})}>
19
35
  <CssBaseline enableColorScheme />
20
- <AppBar />
36
+ <AppBar disableNavigation={state.loading} />
21
37
  <Routes location={state.location}>
22
38
  <Route path="/" element={<HomeView />} />
23
39
  <Route path="/settings" element={<SettingsView />} />
24
40
  <Route path="/development" element={<DevelopmentView />} />
25
41
  </Routes>
42
+ <Popper
43
+ className={Styles.helpPopup}
44
+ open={Boolean(anchorEl)}
45
+ anchorEl={anchorEl}
46
+ disablePortal
47
+ >
48
+ <Paper sx={{p: 2}}>
49
+ <Typography className={Styles.header} color="textSecondary">{help.header}</Typography>
50
+ <Typography component="div" className={Styles.content}>{renderText(help.content)}</Typography>
51
+ </Paper>
52
+ </Popper>
26
53
  </Box>
27
54
  </HashRouter>
28
55
  );
@@ -0,0 +1,7 @@
1
+ export const AlbumsHrefSelector = "//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/a[contains(text(), 'Album')]";
2
+
3
+ export const AlbumFilterSelector = "//ytmusic-app-layout//div[@id='content']/ytmusic-browse-response//ytmusic-section-list-renderer/div[@id='header']//iron-selector//a[contains(@title, 'Album')]";
4
+
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
+
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')]";
@@ -0,0 +1,138 @@
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 {GetYoutubeUrlParams, GetYoutubeUrlResult} from "../common/Messaging";
16
+ import puppeteerOptions from "../common/PuppeteerOptions";
17
+ import {IReporter, ProgressInfo, Reporter} from "../common/Reporter";
18
+ import {
19
+ AlbumFilterSelector, AlbumLinkSelector, AlbumsDirectLinkSelector, AlbumsHrefSelector
20
+ } from "./Selectors";
21
+
22
+ let page: Page;
23
+ let browser: Browser;
24
+ let reporter: IReporter<GetYoutubeUrlResult>;
25
+
26
+ puppeteer.use(StealthPlugin());
27
+
28
+ const navigateToPage = async (url: string) => {
29
+ await page.goto(url, {
30
+ waitUntil: ["networkidle0", "domcontentloaded", "load"],
31
+ timeout: puppeteerOptions.timeout,
32
+ });
33
+ };
34
+
35
+ export const execute = async (
36
+ params: GetYoutubeUrlParams,
37
+ options: LaunchOptions,
38
+ i18n: i18next,
39
+ onProgress: (data: ProgressInfo<GetYoutubeUrlResult>) => void,
40
+ ) => {
41
+ try {
42
+ const result: GetYoutubeUrlResult = {warnings: [], errors: [], urls: []};
43
+ const userAgent = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.3";
44
+
45
+ await i18n.changeLanguage(params.lang);
46
+
47
+ reporter = new Reporter(onProgress);
48
+ reporter.start(i18n.t("starting"));
49
+ browser = await puppeteer.launch(_merge(puppeteerOptions, options));
50
+ [page] = await browser.pages();
51
+
52
+ await page.setUserAgent(userAgent);
53
+
54
+ const cachedCookies = fs.readJSONSync(getProfilePath() + "/cookies.json", {throws: false});
55
+
56
+ if (_isEmpty(cachedCookies)) {
57
+ await waitFor(3000);
58
+ const pageCookies = await page.cookies();
59
+
60
+ fs.writeJSONSync(getProfilePath() + "/cookies.json", pageCookies, {spaces: 2});
61
+
62
+ await page.setCookie(...pageCookies);
63
+ } else {
64
+ await page.setCookie(...cachedCookies);
65
+ }
66
+
67
+ await navigateToPage(params.url);
68
+
69
+ const process = async (urlToProcess: string) => {
70
+ const results: string[] = [];
71
+
72
+ try {
73
+ await navigateToPage(urlToProcess);
74
+
75
+ const element = await page.waitForSelector(`::-p-xpath(${AlbumsHrefSelector})`, {timeout: 1000});
76
+ const albumsUrl = await element.evaluate((el) => el.getAttribute("href"));
77
+
78
+ await navigateToPage(`${params.url}/${albumsUrl}`);
79
+ const albumFilterButton = await page.waitForSelector(`::-p-xpath(${AlbumFilterSelector})`, {timeout: 1000});
80
+
81
+ albumFilterButton.click();
82
+ await page.waitForNetworkIdle();
83
+
84
+ // await page.screenshot({path: './profile/debug.png', fullPage: true});
85
+ // fs.writeFileSync("./profile/debug.html", await page.content());
86
+
87
+ const items = await page.$$eval(`xpath/${AlbumLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
88
+
89
+ for (const item of items) {
90
+ results.push(`${params.url}/${item}`);
91
+ }
92
+
93
+ return results;
94
+ } catch (error) {
95
+ const items = await page.$$eval(`xpath/${AlbumsDirectLinkSelector}`, (elements) => elements.map((el) => el.getAttribute("href")));
96
+
97
+ for (const item of items) {
98
+ results.push(`${params.url}/${item}`);
99
+ }
100
+
101
+ return results;
102
+ }
103
+ };
104
+
105
+ for (const u of params.artistUrls) {
106
+ const data = await process(u);
107
+
108
+ result.urls.push(...data);
109
+ }
110
+
111
+ reporter.finish("done", result);
112
+ } catch (error: any) {
113
+ const result: GetYoutubeUrlResult = {errors: []};
114
+
115
+ if (error instanceof TimeoutError) {
116
+ result.errors.push({title: i18n.t("exceptionTimeout"), description: i18n.t("exceptionTimeoutText")});
117
+ } else {
118
+ result.errors.push({title: i18n.t("exceptionGetYoutubeUrls"), description: i18n.t("exceptionGetYoutubeUrlsText", {error: error.name})});
119
+ }
120
+
121
+ reporter.finish("done", result);
122
+ console.error("Execution failed at stage: ", error.stack);
123
+ } finally {
124
+ await closeResources();
125
+ }
126
+ };
127
+
128
+ const closeResources = async () => {
129
+ if (page) await page.close();
130
+ if (browser) await browser.close();
131
+ };
132
+
133
+ export const cancel = async () => {
134
+ browser.close();
135
+ browser.disconnect();
136
+ };
137
+
138
+ export default execute;