├── .erb ├── configs │ ├── .eslintrc │ ├── webpack.config.base.ts │ ├── webpack.config.eslint.ts │ ├── webpack.config.main.prod.ts │ ├── webpack.config.preload.dev.ts │ ├── webpack.config.renderer.dev.dll.ts │ ├── webpack.config.renderer.dev.ts │ ├── webpack.config.renderer.prod.ts │ └── webpack.paths.ts ├── img │ ├── erb-banner.svg │ ├── erb-logo.png │ └── palette-sponsor-banner.svg ├── mocks │ └── fileMock.js └── scripts │ ├── .eslintrc │ ├── check-build-exists.ts │ ├── check-native-dep.js │ ├── check-node-env.js │ ├── check-port-in-use.js │ ├── clean.js │ ├── delete-source-maps.js │ ├── electron-rebuild.js │ └── link-modules.ts ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ └── 3-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ ├── codeql-analysis.yml │ └── publish.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── assets.d.ts ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ └── 64x64.png ├── components.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── release └── app │ ├── package.json │ └── pnpm-lock.yaml ├── src ├── main │ ├── api │ │ ├── app.router.ts │ │ ├── index.ts │ │ ├── live.router.ts │ │ ├── opensubtitles │ │ │ ├── api.error.ts │ │ │ ├── api.router.ts │ │ │ ├── download.types.ts │ │ │ ├── languages.ts │ │ │ ├── login.types.ts │ │ │ ├── logout.types.ts │ │ │ ├── search.types.ts │ │ │ └── user-information.types.ts │ │ ├── proxy.router.ts │ │ ├── tmdb │ │ │ ├── api.error.ts │ │ │ ├── api.router.ts │ │ │ ├── details.types.ts │ │ │ ├── discover.types.ts │ │ │ ├── genres.ts │ │ │ ├── movie-details.types.ts │ │ │ ├── search.types.ts │ │ │ ├── sort-by.ts │ │ │ └── tvshow-details.types.ts │ │ ├── trpc-client.ts │ │ ├── updater.router.ts │ │ ├── vidsrc.router.ts │ │ └── vlc.router.ts │ ├── extractors │ │ ├── 2embed.ts │ │ ├── closeload.ts │ │ ├── embedsito.ts │ │ ├── filemoon.ts │ │ ├── gomovies.ts │ │ ├── live │ │ │ └── cricfoot2.ts │ │ ├── moviesapi.ts │ │ ├── multimovies.ts │ │ ├── myfilestorage.ts │ │ ├── rabbitstream.ts │ │ ├── remotestream.ts │ │ ├── ridomovies.ts │ │ ├── showbox.ts │ │ ├── smashystream │ │ │ ├── cf.ts │ │ │ ├── dudmovie.ts │ │ │ ├── dued.ts │ │ │ ├── ee.ts │ │ │ ├── ems.ts │ │ │ ├── ffix.ts │ │ │ ├── fizzzz.ts │ │ │ ├── fm22.ts │ │ │ ├── fx.ts │ │ │ ├── im.ts │ │ │ ├── nflim.ts │ │ │ ├── segu.ts │ │ │ ├── smashystream.ts │ │ │ ├── video1.ts │ │ │ ├── video3m.ts │ │ │ └── watchx.ts │ │ ├── streamlare.ts │ │ ├── streamwish.ts │ │ ├── superstream │ │ │ ├── LICENSE │ │ │ ├── superstream.ts │ │ │ └── types.ts │ │ ├── types.ts │ │ ├── uhdmovies.ts │ │ ├── utils.ts │ │ ├── vegamovies │ │ │ ├── aiotechnical.ts │ │ │ ├── gofile.ts │ │ │ └── vegamovies.ts │ │ ├── vidplay.ts │ │ ├── vidsrc.ts │ │ ├── vidsrcto.ts │ │ └── vidstream.ts │ ├── lib │ │ ├── m3u8-proxy.ts │ │ ├── mp4-proxy │ │ │ ├── command.ts │ │ │ └── proxy.js │ │ └── vlc.ts │ ├── main.ts │ ├── preload.ts │ ├── util.ts │ └── utils │ │ ├── axios.ts │ │ ├── crypto.ts │ │ └── vmContext.ts ├── renderer │ ├── App.tsx │ ├── api │ │ └── trpc.ts │ ├── assets │ │ └── undraw_page_not_found.svg │ ├── components │ │ ├── contexts │ │ │ └── theme-provider.tsx │ │ ├── episode-list.tsx │ │ ├── layout │ │ │ ├── color-theme.tsx │ │ │ ├── header.tsx │ │ │ ├── loaders │ │ │ │ └── skeleton-grid.tsx │ │ │ └── theme-toggle.tsx │ │ ├── pagination.tsx │ │ ├── player │ │ │ ├── player.tsx │ │ │ ├── source-selector.tsx │ │ │ ├── subtitle-selector.tsx │ │ │ ├── sync-subtitles.tsx │ │ │ └── utils.tsx │ │ ├── settings │ │ │ ├── opensubtitles.tsx │ │ │ └── sources-check.tsx │ │ ├── show-filters.tsx │ │ ├── show-information.tsx │ │ ├── ui │ │ │ ├── aspect-ratio.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── multi-select.tsx │ │ │ ├── popover.tsx │ │ │ ├── progress.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── spacer.tsx │ │ │ ├── spinner-button.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ ├── tooltip.tsx │ │ │ └── use-toast.ts │ │ └── update-modal.tsx │ ├── constants.ts │ ├── global.d.ts │ ├── hooks │ │ ├── useLocalStorage.ts │ │ ├── useQuery.ts │ │ └── useRequiredParams.ts │ ├── index.ejs │ ├── index.tsx │ ├── lib │ │ ├── proxy.ts │ │ └── utils.ts │ ├── pages │ │ ├── index.tsx │ │ ├── live │ │ │ ├── list.tsx │ │ │ └── view.tsx │ │ ├── settings.tsx │ │ └── shows │ │ │ ├── discover.tsx │ │ │ ├── search.tsx │ │ │ └── view.tsx │ ├── preload.d.ts │ ├── styles │ │ └── globals.css │ └── utils │ │ └── string.ts └── types │ ├── localstorage.ts │ ├── sources.ts │ └── tmbd.ts ├── tailwind.config.js └── tsconfig.json /.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.base.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import webpack from "webpack"; 6 | import TsconfigPathsPlugins from "tsconfig-paths-webpack-plugin"; 7 | import webpackPaths from "./webpack.paths"; 8 | import { dependencies as externals } from "../../release/app/package.json"; 9 | 10 | const configuration: webpack.Configuration = { 11 | externals: [...Object.keys(externals || {})], 12 | 13 | stats: "errors-only", 14 | 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.[jt]sx?$/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: "ts-loader", 22 | options: { 23 | // Remove this line to enable type checking in webpack builds 24 | transpileOnly: true, 25 | compilerOptions: { 26 | module: "esnext", 27 | }, 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | output: { 35 | path: webpackPaths.srcPath, 36 | // https://github.com/webpack/webpack/issues/1114 37 | library: { 38 | type: "commonjs2", 39 | }, 40 | }, 41 | 42 | /** 43 | * Determine the array of extensions that should be used to resolve modules. 44 | */ 45 | resolve: { 46 | extensions: [".js", ".jsx", ".json", ".ts", ".tsx"], 47 | modules: [webpackPaths.srcPath, "node_modules"], 48 | // There is no need to add aliases here, the paths in tsconfig get mirrored 49 | plugins: [new TsconfigPathsPlugins()], 50 | fallback: { 51 | buffer: require.resolve("buffer"), 52 | }, 53 | }, 54 | 55 | plugins: [ 56 | new webpack.EnvironmentPlugin({ 57 | NODE_ENV: "production", 58 | }), 59 | new webpack.ProvidePlugin({ 60 | Buffer: ["buffer", "Buffer"], 61 | }), 62 | new webpack.DefinePlugin({ 63 | OPENSUBTITLES_API_KEY: JSON.stringify(process.env.OPENSUBTITLES_API_KEY), 64 | TMDB_API_KEY: JSON.stringify(process.env.TMDB_API_KEY), 65 | }), 66 | ], 67 | }; 68 | 69 | export default configuration; 70 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require("./webpack.config.renderer.dev").default; 4 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.main.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from "path"; 6 | import webpack from "webpack"; 7 | import { merge } from "webpack-merge"; 8 | import TerserPlugin from "terser-webpack-plugin"; 9 | import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; 10 | import baseConfig from "./webpack.config.base"; 11 | import webpackPaths from "./webpack.paths"; 12 | import checkNodeEnv from "../scripts/check-node-env"; 13 | import deleteSourceMaps from "../scripts/delete-source-maps"; 14 | 15 | checkNodeEnv("production"); 16 | deleteSourceMaps(); 17 | 18 | const configuration: webpack.Configuration = { 19 | devtool: "source-map", 20 | 21 | mode: "production", 22 | 23 | target: "electron-main", 24 | 25 | entry: { 26 | main: path.join(webpackPaths.srcMainPath, "main.ts"), 27 | preload: path.join(webpackPaths.srcMainPath, "preload.ts"), 28 | }, 29 | 30 | output: { 31 | path: webpackPaths.distMainPath, 32 | filename: "[name].js", 33 | library: { 34 | type: "umd", 35 | }, 36 | }, 37 | 38 | optimization: { 39 | minimizer: [ 40 | new TerserPlugin({ 41 | parallel: true, 42 | }), 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: process.env.ANALYZE === "true" ? "server" : "disabled", 49 | analyzerPort: 8888, 50 | }), 51 | 52 | /** 53 | * Create global constants which can be configured at compile time. 54 | * 55 | * Useful for allowing different behaviour between development builds and 56 | * release builds 57 | * 58 | * NODE_ENV should be production so that modules do not perform certain 59 | * development checks 60 | */ 61 | new webpack.EnvironmentPlugin({ 62 | NODE_ENV: "production", 63 | DEBUG_PROD: false, 64 | START_MINIMIZED: false, 65 | }), 66 | 67 | new webpack.DefinePlugin({ 68 | "process.type": '"browser"', 69 | OPENSUBTITLES_API_KEY: JSON.stringify(process.env.OPENSUBTITLES_API_KEY), 70 | TMDB_API_KEY: JSON.stringify(process.env.TMDB_API_KEY), 71 | }), 72 | ], 73 | 74 | /** 75 | * Disables webpack processing of __dirname and __filename. 76 | * If you run the bundle in node.js it falls back to these values of node.js. 77 | * https://github.com/webpack/webpack/issues/2010 78 | */ 79 | node: { 80 | __dirname: false, 81 | __filename: false, 82 | }, 83 | }; 84 | 85 | export default merge(baseConfig, configuration); 86 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.preload.dev.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | import { merge } from "webpack-merge"; 4 | import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; 5 | import baseConfig from "./webpack.config.base"; 6 | import webpackPaths from "./webpack.paths"; 7 | import checkNodeEnv from "../scripts/check-node-env"; 8 | 9 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 10 | // at the dev webpack config is not accidentally run in a production environment 11 | if (process.env.NODE_ENV === "production") { 12 | checkNodeEnv("development"); 13 | } 14 | 15 | const configuration: webpack.Configuration = { 16 | devtool: "inline-source-map", 17 | 18 | mode: "development", 19 | 20 | target: "electron-preload", 21 | 22 | entry: path.join(webpackPaths.srcMainPath, "preload.ts"), 23 | 24 | output: { 25 | path: webpackPaths.dllPath, 26 | filename: "preload.js", 27 | library: { 28 | type: "umd", 29 | }, 30 | }, 31 | 32 | plugins: [ 33 | new BundleAnalyzerPlugin({ 34 | analyzerMode: process.env.ANALYZE === "true" ? "server" : "disabled", 35 | }), 36 | 37 | /** 38 | * Create global constants which can be configured at compile time. 39 | * 40 | * Useful for allowing different behaviour between development builds and 41 | * release builds 42 | * 43 | * NODE_ENV should be production so that modules do not perform certain 44 | * development checks 45 | * 46 | * By default, use 'development' as NODE_ENV. This can be overriden with 47 | * 'staging', for example, by changing the ENV variables in the npm scripts 48 | */ 49 | new webpack.EnvironmentPlugin({ 50 | NODE_ENV: "development", 51 | }), 52 | 53 | new webpack.LoaderOptionsPlugin({ 54 | debug: true, 55 | }), 56 | ], 57 | 58 | /** 59 | * Disables webpack processing of __dirname and __filename. 60 | * If you run the bundle in node.js it falls back to these values of node.js. 61 | * https://github.com/webpack/webpack/issues/2010 62 | */ 63 | node: { 64 | __dirname: false, 65 | __filename: false, 66 | }, 67 | 68 | watch: true, 69 | }; 70 | 71 | export default merge(baseConfig, configuration); 72 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.dev.dll.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from "webpack"; 6 | import path from "path"; 7 | import { merge } from "webpack-merge"; 8 | import baseConfig from "./webpack.config.base"; 9 | import webpackPaths from "./webpack.paths"; 10 | import { dependencies } from "../../package.json"; 11 | import checkNodeEnv from "../scripts/check-node-env"; 12 | import webpackConfigRendererDev from "./webpack.config.renderer.dev"; 13 | 14 | checkNodeEnv("development"); 15 | 16 | const dist = webpackPaths.dllPath; 17 | 18 | const configuration: webpack.Configuration = { 19 | context: webpackPaths.rootPath, 20 | 21 | devtool: "eval", 22 | 23 | mode: "development", 24 | 25 | target: "electron-renderer", 26 | 27 | externals: ["fsevents", "crypto-browserify"], 28 | 29 | /** 30 | * Use `module` from `webpack.config.renderer.dev.js` 31 | */ 32 | module: webpackConfigRendererDev.module, 33 | 34 | entry: { 35 | renderer: Object.keys(dependencies || {}) 36 | .filter((it) => it !== "electron-trpc") 37 | .filter((it) => it !== "@warren-bank/hls-proxy"), 38 | }, 39 | 40 | output: { 41 | path: dist, 42 | filename: "[name].dev.dll.js", 43 | library: { 44 | name: "renderer", 45 | type: "var", 46 | }, 47 | }, 48 | 49 | plugins: [ 50 | new webpack.DllPlugin({ 51 | path: path.join(dist, "[name].json"), 52 | name: "[name]", 53 | }), 54 | 55 | /** 56 | * Create global constants which can be configured at compile time. 57 | * 58 | * Useful for allowing different behaviour between development builds and 59 | * release builds 60 | * 61 | * NODE_ENV should be production so that modules do not perform certain 62 | * development checks 63 | */ 64 | new webpack.EnvironmentPlugin({ 65 | NODE_ENV: "development", 66 | }), 67 | 68 | new webpack.LoaderOptionsPlugin({ 69 | debug: true, 70 | options: { 71 | context: webpackPaths.srcPath, 72 | output: { 73 | path: webpackPaths.dllPath, 74 | }, 75 | }, 76 | }), 77 | ], 78 | }; 79 | 80 | export default merge(baseConfig, configuration); 81 | -------------------------------------------------------------------------------- /.erb/configs/webpack.config.renderer.prod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from "path"; 6 | import webpack from "webpack"; 7 | import HtmlWebpackPlugin from "html-webpack-plugin"; 8 | import MiniCssExtractPlugin from "mini-css-extract-plugin"; 9 | import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; 10 | import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; 11 | import { merge } from "webpack-merge"; 12 | import TerserPlugin from "terser-webpack-plugin"; 13 | import baseConfig from "./webpack.config.base"; 14 | import webpackPaths from "./webpack.paths"; 15 | import checkNodeEnv from "../scripts/check-node-env"; 16 | import deleteSourceMaps from "../scripts/delete-source-maps"; 17 | 18 | checkNodeEnv("production"); 19 | deleteSourceMaps(); 20 | 21 | const configuration: webpack.Configuration = { 22 | devtool: "source-map", 23 | 24 | mode: "production", 25 | 26 | target: ["web", "electron-renderer"], 27 | 28 | entry: [path.join(webpackPaths.srcRendererPath, "index.tsx")], 29 | 30 | output: { 31 | path: webpackPaths.distRendererPath, 32 | publicPath: "./", 33 | filename: "renderer.js", 34 | library: { 35 | type: "umd", 36 | }, 37 | }, 38 | 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.s?(a|c)ss$/, 43 | use: [ 44 | MiniCssExtractPlugin.loader, 45 | { 46 | loader: "css-loader", 47 | options: { 48 | modules: true, 49 | sourceMap: true, 50 | importLoaders: 1, 51 | }, 52 | }, 53 | "sass-loader", 54 | ], 55 | include: /\.module\.s?(c|a)ss$/, 56 | }, 57 | { 58 | test: /\.s?(a|c)ss$/, 59 | use: [ 60 | MiniCssExtractPlugin.loader, 61 | "css-loader", 62 | "postcss-loader", 63 | "sass-loader", 64 | ], 65 | exclude: /\.module\.s?(c|a)ss$/, 66 | }, 67 | // Fonts 68 | { 69 | test: /\.(woff|woff2|eot|ttf|otf)$/i, 70 | type: "asset/resource", 71 | }, 72 | // Images 73 | { 74 | test: /\.(png|jpg|jpeg|gif)$/i, 75 | type: "asset/resource", 76 | }, 77 | // SVG 78 | { 79 | test: /\.svg$/, 80 | use: [ 81 | { 82 | loader: "@svgr/webpack", 83 | options: { 84 | prettier: false, 85 | svgo: false, 86 | svgoConfig: { 87 | plugins: [{ removeViewBox: false }], 88 | }, 89 | titleProp: true, 90 | ref: true, 91 | }, 92 | }, 93 | "file-loader", 94 | ], 95 | }, 96 | ], 97 | }, 98 | 99 | optimization: { 100 | minimize: true, 101 | minimizer: [new TerserPlugin(), new CssMinimizerPlugin()], 102 | }, 103 | 104 | plugins: [ 105 | /** 106 | * Create global constants which can be configured at compile time. 107 | * 108 | * Useful for allowing different behaviour between development builds and 109 | * release builds 110 | * 111 | * NODE_ENV should be production so that modules do not perform certain 112 | * development checks 113 | */ 114 | new webpack.EnvironmentPlugin({ 115 | NODE_ENV: "production", 116 | DEBUG_PROD: false, 117 | }), 118 | 119 | new MiniCssExtractPlugin({ 120 | filename: "style.css", 121 | }), 122 | 123 | new BundleAnalyzerPlugin({ 124 | analyzerMode: process.env.ANALYZE === "true" ? "server" : "disabled", 125 | analyzerPort: 8889, 126 | }), 127 | 128 | new HtmlWebpackPlugin({ 129 | filename: "index.html", 130 | template: path.join(webpackPaths.srcRendererPath, "index.ejs"), 131 | minify: { 132 | collapseWhitespace: true, 133 | removeAttributeQuotes: true, 134 | removeComments: true, 135 | }, 136 | isBrowser: false, 137 | isDevelopment: false, 138 | }), 139 | 140 | new webpack.DefinePlugin({ 141 | "process.type": '"renderer"', 142 | OPENSUBTITLES_API_KEY: JSON.stringify(process.env.OPENSUBTITLES_API_KEY), 143 | TMDB_API_KEY: JSON.stringify(process.env.TMDB_API_KEY), 144 | }), 145 | ], 146 | }; 147 | 148 | export default merge(baseConfig, configuration); 149 | -------------------------------------------------------------------------------- /.erb/configs/webpack.paths.ts: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const rootPath = path.join(__dirname, "../.."); 4 | 5 | const dllPath = path.join(__dirname, "../dll"); 6 | 7 | const srcPath = path.join(rootPath, "src"); 8 | const srcMainPath = path.join(srcPath, "main"); 9 | const srcRendererPath = path.join(srcPath, "renderer"); 10 | 11 | const releasePath = path.join(rootPath, "release"); 12 | const appPath = path.join(releasePath, "app"); 13 | const appPackagePath = path.join(appPath, "package.json"); 14 | const appNodeModulesPath = path.join(appPath, "node_modules"); 15 | const srcNodeModulesPath = path.join(srcPath, "node_modules"); 16 | 17 | const distPath = path.join(appPath, "dist"); 18 | const distMainPath = path.join(distPath, "main"); 19 | const distRendererPath = path.join(distPath, "renderer"); 20 | const distProxyPath = path.join(distPath, "proxy"); 21 | 22 | const buildPath = path.join(releasePath, "build"); 23 | 24 | export default { 25 | rootPath, 26 | dllPath, 27 | srcPath, 28 | srcMainPath, 29 | srcRendererPath, 30 | releasePath, 31 | appPath, 32 | appPackagePath, 33 | appNodeModulesPath, 34 | srcNodeModulesPath, 35 | distPath, 36 | distMainPath, 37 | distRendererPath, 38 | distProxyPath, 39 | buildPath, 40 | }; 41 | -------------------------------------------------------------------------------- /.erb/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/.erb/img/erb-logo.png -------------------------------------------------------------------------------- /.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default "test-file-stub"; 2 | -------------------------------------------------------------------------------- /.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from "path"; 3 | import chalk from "chalk"; 4 | import fs from "fs"; 5 | import webpackPaths from "../configs/webpack.paths"; 6 | 7 | const mainPath = path.join(webpackPaths.distMainPath, "main.js"); 8 | const rendererPath = path.join(webpackPaths.distRendererPath, "renderer.js"); 9 | 10 | if (!fs.existsSync(mainPath)) { 11 | throw new Error( 12 | chalk.whiteBright.bgRed.bold( 13 | 'The main process is not built yet. Build it by running "npm run build:main"', 14 | ), 15 | ); 16 | } 17 | 18 | if (!fs.existsSync(rendererPath)) { 19 | throw new Error( 20 | chalk.whiteBright.bgRed.bold( 21 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"', 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.erb/scripts/check-native-dep.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import chalk from "chalk"; 3 | import { execSync } from "child_process"; 4 | import { dependencies } from "../../package.json"; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync("node_modules") 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | if (nativeDeps.length === 0) { 12 | process.exit(0); 13 | } 14 | try { 15 | // Find the reason for why the dependency is installed. If it is installed 16 | // because of a devDependency then that is okay. Warn when it is installed 17 | // because of a dependency 18 | const { dependencies: dependenciesObject } = JSON.parse( 19 | execSync(`npm ls ${nativeDeps.join(" ")} --json`).toString(), 20 | ); 21 | const rootDependencies = Object.keys(dependenciesObject); 22 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 23 | dependenciesKeys.includes(rootDependency), 24 | ); 25 | if (filteredRootDependencies.length > 0) { 26 | const plural = filteredRootDependencies.length > 1; 27 | console.log(` 28 | ${chalk.whiteBright.bgYellow.bold( 29 | "Webpack does not work with native dependencies.", 30 | )} 31 | ${chalk.bold(filteredRootDependencies.join(", "))} ${ 32 | plural ? "are native dependencies" : "is a native dependency" 33 | } and should be installed inside of the "./release/app" folder. 34 | First, uninstall the packages from "./package.json": 35 | ${chalk.whiteBright.bgGreen.bold("npm uninstall your-package")} 36 | ${chalk.bold( 37 | 'Then, instead of installing the package to the root "./package.json":', 38 | )} 39 | ${chalk.whiteBright.bgRed.bold("npm install your-package")} 40 | ${chalk.bold('Install the package to "./release/app/package.json"')} 41 | ${chalk.whiteBright.bgGreen.bold( 42 | "cd ./release/app && npm install your-package", 43 | )} 44 | Read more about native dependencies at: 45 | ${chalk.bold( 46 | "https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure", 47 | )} 48 | `); 49 | process.exit(1); 50 | } 51 | } catch (e) { 52 | console.log("Native dependencies could not be checked"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`, 12 | ), 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import detectPort from "detect-port"; 3 | 4 | const port = process.env.PORT || "1212"; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start`, 11 | ), 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import { rimrafSync } from "rimraf"; 2 | import fs from "fs"; 3 | import webpackPaths from "../configs/webpack.paths"; 4 | 5 | const foldersToRemove = [ 6 | webpackPaths.distPath, 7 | webpackPaths.buildPath, 8 | webpackPaths.dllPath, 9 | ]; 10 | 11 | foldersToRemove.forEach((folder) => { 12 | if (fs.existsSync(folder)) rimrafSync(folder); 13 | }); 14 | -------------------------------------------------------------------------------- /.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { rimrafSync } from "rimraf"; 4 | import webpackPaths from "../configs/webpack.paths"; 5 | 6 | export default function deleteSourceMaps() { 7 | if (fs.existsSync(webpackPaths.distMainPath)) 8 | rimrafSync(path.join(webpackPaths.distMainPath, "*.js.map"), { 9 | glob: true, 10 | }); 11 | if (fs.existsSync(webpackPaths.distRendererPath)) 12 | rimrafSync(path.join(webpackPaths.distRendererPath, "*.js.map"), { 13 | glob: true, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import fs from "fs"; 3 | import { dependencies } from "../../release/app/package.json"; 4 | import webpackPaths from "../configs/webpack.paths"; 5 | 6 | if ( 7 | Object.keys(dependencies || {}).length > 0 && 8 | fs.existsSync(webpackPaths.appNodeModulesPath) 9 | ) { 10 | const electronRebuildCmd = 11 | "../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir ."; 12 | const cmd = 13 | process.platform === "win32" 14 | ? electronRebuildCmd.replace(/\//g, "\\") 15 | : electronRebuildCmd; 16 | execSync(cmd, { 17 | cwd: webpackPaths.appPath, 18 | stdio: "inherit", 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import webpackPaths from "../configs/webpack.paths"; 3 | 4 | const { srcNodeModulesPath } = webpackPaths; 5 | const { appNodeModulesPath } = webpackPaths; 6 | 7 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 8 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, "junction"); 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb 34 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "erb", 3 | rules: { 4 | // A temporary hack related to IDE not resolving correct package.json 5 | "import/no-extraneous-dependencies": "off", 6 | "import/no-unresolved": "error", 7 | // Since React 17 and typescript 4.1 you can safely disable the rule 8 | "react/react-in-jsx-scope": "off", 9 | "react/jsx-filename-extension": [1, { extensions: [".tsx", ".ts"] }], 10 | "import/extensions": "off", 11 | "react/function-component-definition": [ 12 | 2, 13 | { 14 | namedComponents: "arrow-function", 15 | unnamedComponents: "arrow-function", 16 | }, 17 | ], 18 | "no-unused-vars": "warn", 19 | "import/prefer-default-export": "off", 20 | "react/require-default-props": "off", 21 | "jsx-a11y/label-has-associated-control": "off", 22 | "class-methods-use-this": "off", 23 | "promise/catch-or-return": "off", 24 | "promise/always-return": "off", 25 | "consistent-return": "off", 26 | "prefer-destructuring": "off", 27 | "no-bitwise": "off", 28 | printWidth: "off", 29 | "no-restricted-syntax": "off", 30 | "react/prop-types": "off", 31 | "react/jsx-props-no-spreading": "off", 32 | "jsx-a11y/heading-has-content": "off", 33 | "@typescript-eslint/no-use-before-define": "off", 34 | "@typescript-eslint/no-shadow": "off", 35 | }, 36 | parserOptions: { 37 | ecmaVersion: 2020, 38 | sourceType: "module", 39 | project: "./tsconfig.json", 40 | tsconfigRootDir: __dirname, 41 | createDefaultProgram: true, 42 | }, 43 | settings: { 44 | "import/resolver": { 45 | // See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below 46 | node: {}, 47 | webpack: { 48 | config: require.resolve("./.erb/configs/webpack.config.eslint.ts"), 49 | }, 50 | typescript: {}, 51 | }, 52 | "import/parsers": { 53 | "@typescript-eslint/parser": [".ts", ".tsx"], 54 | }, 55 | }, 56 | globals: { 57 | NodeJS: true, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: "bug" 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using npm 14 | - [ ] Using an up-to-date [`main` branch](https://github.com/JorrinKievit/restreamer/tree/main) 15 | - [ ] Tried solutions mentioned in [#400](https://github.com/JorrinKievit/restreamer/issues/400) 16 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true npm run build && npm start` 17 | 18 | ## Expected Behavior 19 | 20 | 21 | 22 | ## Current Behavior 23 | 24 | 25 | 26 | ## Steps to Reproduce 27 | 28 | 29 | 30 | 31 | 1. 32 | 33 | 2. 34 | 35 | 3. 36 | 37 | 4. 38 | 39 | ## Possible Solution (Not obligatory) 40 | 41 | 42 | 43 | ## Context 44 | 45 | 46 | 47 | 48 | ## Your Environment 49 | 50 | 51 | 52 | - Node version : 53 | - restreamer version or branch : 54 | - Operating System and version : 55 | - Link to your project : 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: "question" 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the app. 4 | labels: "enhancement" 5 | --- 6 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - discussion 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["main"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["main"] 20 | schedule: 21 | - cron: "44 16 * * 4" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v2 59 | 60 | # ℹ️ Command-line programs to run using the OS shell. 61 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 62 | 63 | # If the Autobuild fails above, remove it and uncomment the following three lines. 64 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 65 | 66 | # - run: | 67 | # echo "Run, Build Application using script" 68 | # ./location_of_script_within_repo/buildscript.sh 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | publish: 10 | # To enable auto publishing to github, update your electron publisher 11 | # config in package.json > "build" and remove the conditional below 12 | if: ${{ github.repository_owner == 'JorrinKievit' }} 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [macos-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: pnpm/action-setup@v2 23 | with: 24 | version: 7 25 | - name: Install Node and pnpm 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | cache: pnpm 30 | 31 | - name: Install and build 32 | run: | 33 | export TMDB_API_KEY=${{ secrets.TMDB_API_KEY }} 34 | export OPENSUBTITLES_API_KEY=${{ secrets.OPENSUBTITLES_API_KEY }} 35 | pnpm install --frozen-lockfile 36 | pnpm run postinstall 37 | pnpm run build 38 | 39 | - name: Publish releases 40 | env: 41 | # These values are used for auto updates signing 42 | # APPLE_ID: ${{ secrets.APPLE_ID }} 43 | # APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }} 44 | # CSC_LINK: ${{ secrets.CSC_LINK }} 45 | # CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} 46 | # This is used for uploading release assets to github 47 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | run: | 49 | pnpm exec electron-builder --publish always --win --linux --mac 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | .env 32 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | node-linker=hoisted 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "plugins": ["prettier-plugin-tailwindcss"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run", "start"], 11 | "env": { 12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 13 | } 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | }, 30 | "typescript.tsdk": "node_modules\\typescript\\lib", 31 | "tailwindCSS.experimental.classRegex": [ 32 | ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], 33 | ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present JorrinKievit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Restreamer 2 | 3 | Desktop app for streaming movies and TV shows 4 | 5 | ## Saves watch progress 6 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/1d032f5e-a8bc-474e-9ff1-cf45f648965f) 7 | 8 | ## Searching for movies and tv shows 9 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/dd3671db-2acd-43f3-a9cd-a5f7e8868846) 10 | 11 | ## Viewing the movies with episodes and general information 12 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/63b01284-7ab4-4f6c-ac0a-20e001c12228) 13 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/2d98e19c-f0e1-4919-97c3-9f93d727bc09) 14 | 15 | 16 | ## Settings 17 | - Support for OpenSubtitles. 18 | - A check to see which sources are operational. 19 | - Selecting different themes 20 | 21 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/0944a333-a078-4404-a205-3487bf216491) 22 | 23 | ## Light mode 24 | ![image](https://github.com/JorrinKievit/restreamer/assets/43169049/a2316662-a1f1-435d-9926-d3fb418f4719) 25 | -------------------------------------------------------------------------------- /assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module "*.svg" { 4 | export const ReactComponent: React.FC>; 5 | 6 | const content: string; 7 | export default content; 8 | } 9 | 10 | declare module "*.png" { 11 | const content: string; 12 | export default content; 13 | } 14 | 15 | declare module "*.jpg" { 16 | const content: string; 17 | export default content; 18 | } 19 | 20 | declare module "*.scss" { 21 | const content: Styles; 22 | export default content; 23 | } 24 | 25 | declare module "*.sass" { 26 | const content: Styles; 27 | export default content; 28 | } 29 | 30 | declare module "*.css" { 31 | const content: Styles; 32 | export default content; 33 | } 34 | -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icon.png -------------------------------------------------------------------------------- /assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/128x128.png -------------------------------------------------------------------------------- /assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/16x16.png -------------------------------------------------------------------------------- /assets/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/24x24.png -------------------------------------------------------------------------------- /assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/256x256.png -------------------------------------------------------------------------------- /assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/32x32.png -------------------------------------------------------------------------------- /assets/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/48x48.png -------------------------------------------------------------------------------- /assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/512x512.png -------------------------------------------------------------------------------- /assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JorrinKievit/restreamer/e4e515554ae599282502391bddcf69a5300bea36/assets/icons/64x64.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "./renderer/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "renderer/components", 14 | "utils": "renderer/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restreamer", 3 | "version": "1.5.3", 4 | "description": "Desktop app for streaming movies and TV shows", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Jorrin Kievit", 8 | "email": "jorrin@jorrinkievit.xyz" 9 | }, 10 | "main": "./dist/main/main.js", 11 | "scripts": { 12 | "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 13 | "postinstall": "npm run rebuild && npm run link-modules", 14 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 15 | }, 16 | "dependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /release/app/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | -------------------------------------------------------------------------------- /src/main/api/index.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "./app.router"; 2 | import { liveRouter } from "./live.router"; 3 | import { openSubtitlesRouter } from "./opensubtitles/api.router"; 4 | import { proxyRouter } from "./proxy.router"; 5 | import { tmdbRouter } from "./tmdb/api.router"; 6 | import { t } from "./trpc-client"; 7 | import { updaterRouter } from "./updater.router"; 8 | import { vidSrcRouter } from "./vidsrc.router"; 9 | import { vlcRouter } from "./vlc.router"; 10 | 11 | export const router = t.router({ 12 | app: appRouter, 13 | live: liveRouter, 14 | updater: updaterRouter, 15 | proxy: proxyRouter, 16 | vidsrc: vidSrcRouter, 17 | tmdb: tmdbRouter, 18 | opensubtitles: openSubtitlesRouter, 19 | vlc: vlcRouter, 20 | }); 21 | 22 | export type AppRouter = typeof router; 23 | -------------------------------------------------------------------------------- /src/main/api/live.router.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { CricFoot2Extractor } from "../extractors/live/cricfoot2"; 3 | import { t } from "./trpc-client"; 4 | 5 | export const liveRouter = t.router({ 6 | getMainPage: t.procedure.query(async () => { 7 | const cricFoot2Extractor = new CricFoot2Extractor(); 8 | const sources = await cricFoot2Extractor.getMainPage(); 9 | 10 | return sources; 11 | }), 12 | 13 | getLiveUrl: t.procedure 14 | .input( 15 | z.object({ 16 | url: z.string(), 17 | }), 18 | ) 19 | .query(async (req) => { 20 | const cricFoot2Extractor = new CricFoot2Extractor(); 21 | const source = await cricFoot2Extractor.extractUrls(req.input.url); 22 | 23 | return source; 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/api.error.ts: -------------------------------------------------------------------------------- 1 | export interface ApiError { 2 | status: number; 3 | message: string; 4 | } 5 | 6 | export interface DownloadApiError { 7 | requests: number; 8 | remaining: number; 9 | message: string; 10 | reset_time: string; 11 | reset_time_utc: Date; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/download.types.ts: -------------------------------------------------------------------------------- 1 | export interface DownloadResponse { 2 | link: string; 3 | file_name: string; 4 | requests: number; 5 | remaining: number; 6 | message: string; 7 | reset_time: string; 8 | reset_time_utc: Date; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/login.types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | allowed_downloads: number; 3 | level: string; 4 | user_id: number; 5 | ext_installed: boolean; 6 | vip: boolean; 7 | } 8 | 9 | export interface LoginResponse { 10 | user: User; 11 | token: string; 12 | status: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/logout.types.ts: -------------------------------------------------------------------------------- 1 | export interface LogoutResponse { 2 | status: number; 3 | message: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/search.types.ts: -------------------------------------------------------------------------------- 1 | export interface Uploader { 2 | uploader_id: number; 3 | name: string; 4 | rank: string; 5 | } 6 | 7 | export interface FeatureDetails { 8 | feature_id: number; 9 | feature_type: string; 10 | year: number; 11 | title: string; 12 | movie_name: string; 13 | imdb_id: number; 14 | tmdb_id: number; 15 | } 16 | 17 | export interface RelatedLink { 18 | label: string; 19 | url: string; 20 | img_url: string; 21 | } 22 | 23 | export interface File { 24 | file_id: number; 25 | cd_number: number; 26 | file_name: string; 27 | } 28 | 29 | export interface Attributes { 30 | subtitle_id: string; 31 | language: string; 32 | download_count: number; 33 | new_download_count: number; 34 | hearing_impaired: boolean; 35 | hd: boolean; 36 | fps: number; 37 | votes: number; 38 | points: number; 39 | ratings: number; 40 | from_trusted: boolean; 41 | foreign_parts_only: boolean; 42 | ai_translated: boolean; 43 | machine_translated: boolean; 44 | upload_date: Date; 45 | release: string; 46 | comments: string; 47 | legacy_subtitle_id: number; 48 | uploader: Uploader; 49 | feature_details: FeatureDetails; 50 | url: string; 51 | related_links: RelatedLink[]; 52 | files: File[]; 53 | } 54 | 55 | export interface Datum { 56 | id: string; 57 | type: string; 58 | attributes: Attributes; 59 | } 60 | 61 | export interface SearchResponse { 62 | total_pages: number; 63 | total_count: number; 64 | page: number; 65 | data: Datum[]; 66 | } 67 | -------------------------------------------------------------------------------- /src/main/api/opensubtitles/user-information.types.ts: -------------------------------------------------------------------------------- 1 | export interface UserInformationResponse { 2 | data: { 3 | allowed_downloads: number; 4 | allowed_translations: number; 5 | downloads_count: number; 6 | ext_installed: boolean; 7 | level: string; 8 | remaining_downloads: number; 9 | user_id: number; 10 | username: string; 11 | vip: boolean; 12 | }; 13 | } 14 | 15 | export interface OpenSubtitlesUser { 16 | token: string; 17 | user: { 18 | allowed_downloads: number; 19 | allowed_translations: number; 20 | downloads_count: number; 21 | ext_installed: boolean; 22 | level: string; 23 | remaining_downloads: number; 24 | user_id: number; 25 | username: string; 26 | vip: boolean; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/api/proxy.router.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import z from "zod"; 3 | import { startProxy, stopProxy } from "../lib/mp4-proxy/command"; 4 | import { startM3U8Proxy, stopM3U8Proxy } from "../lib/m3u8-proxy"; 5 | import { t } from "./trpc-client"; 6 | 7 | export const proxyRouter = t.router({ 8 | start: t.procedure 9 | .input( 10 | z.object({ 11 | type: z.enum(["mp4", "m3u8", "mkv"]), 12 | referer: z.string().optional(), 13 | origin: z.string().optional().nullable(), 14 | userAgent: z.string().optional(), 15 | }), 16 | ) 17 | .mutation(({ input }) => { 18 | if (input.type === "mp4") { 19 | startProxy(); 20 | return; 21 | } 22 | startM3U8Proxy({ 23 | referer: input.referer, 24 | origin: input.origin, 25 | userAgent: input.userAgent, 26 | }); 27 | }), 28 | stop: t.procedure.mutation(() => { 29 | stopProxy(); 30 | stopM3U8Proxy(); 31 | }), 32 | validate: t.procedure 33 | .input( 34 | z.object({ 35 | url: z.string(), 36 | }), 37 | ) 38 | .query(async ({ input }) => { 39 | const uri = new URL(input.url); 40 | const { host } = uri; 41 | 42 | let referer = `${host.split(".").slice(-2).join(".")}/`; 43 | referer = `https://${referer}`; 44 | 45 | const res = await axios.get(input.url, { headers: { referer } }); 46 | return res.data; 47 | }), 48 | }); 49 | -------------------------------------------------------------------------------- /src/main/api/tmdb/api.error.ts: -------------------------------------------------------------------------------- 1 | export interface TmdbApiError { 2 | status_code: number; 3 | status_message: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/api/tmdb/details.types.ts: -------------------------------------------------------------------------------- 1 | import { MovieDetailsResults } from "./movie-details.types"; 2 | import { TvShowDetailsResults } from "./tvshow-details.types"; 3 | 4 | type MovieDetailsResultWithMediaType = MovieDetailsResults & { 5 | media_type: "movie"; 6 | }; 7 | 8 | type TvShowDetailsResultWithMediaType = TvShowDetailsResults & { 9 | media_type: "tv"; 10 | }; 11 | 12 | export type MovieOrTvShowDetailsResult = 13 | | MovieDetailsResultWithMediaType 14 | | TvShowDetailsResultWithMediaType; 15 | 16 | export type MovieOrTvShowDetailsResults = MovieOrTvShowDetailsResult[]; 17 | -------------------------------------------------------------------------------- /src/main/api/tmdb/discover.types.ts: -------------------------------------------------------------------------------- 1 | export interface MovieResults { 2 | backdrop_path: string; 3 | first_air_date: string; 4 | genre_ids: number[]; 5 | id: number; 6 | media_type: string; 7 | name: string; 8 | original_language: string; 9 | original_name: string; 10 | overview: string; 11 | popularity: number; 12 | poster_path: string; 13 | vote_average: number; 14 | vote_count: number; 15 | adult?: boolean; 16 | original_title: string; 17 | release_date: string; 18 | title: string; 19 | video?: boolean; 20 | } 21 | 22 | export interface DiscoverMovieResults { 23 | page: number; 24 | results: MovieResults[]; 25 | total_pages: number; 26 | total_results: number; 27 | } 28 | 29 | export interface TvShowResults { 30 | poster_path: string; 31 | popularity: number; 32 | id: number; 33 | backdrop_path: string; 34 | vote_average: number; 35 | overview: string; 36 | first_air_date: string; 37 | origin_country: string[]; 38 | genre_ids: number[]; 39 | original_language: string; 40 | vote_count: number; 41 | name: string; 42 | original_name: string; 43 | } 44 | 45 | export interface DiscoverTvShowsResults { 46 | page: number; 47 | results: MovieResults[]; 48 | total_pages: number; 49 | total_results: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/main/api/tmdb/genres.ts: -------------------------------------------------------------------------------- 1 | export const TMDB_MOVIE_GENRES = [ 2 | { 3 | id: 28, 4 | name: "Action", 5 | }, 6 | { 7 | id: 12, 8 | name: "Adventure", 9 | }, 10 | { 11 | id: 16, 12 | name: "Animation", 13 | }, 14 | { 15 | id: 35, 16 | name: "Comedy", 17 | }, 18 | { 19 | id: 80, 20 | name: "Crime", 21 | }, 22 | { 23 | id: 99, 24 | name: "Documentary", 25 | }, 26 | { 27 | id: 18, 28 | name: "Drama", 29 | }, 30 | { 31 | id: 10751, 32 | name: "Family", 33 | }, 34 | { 35 | id: 14, 36 | name: "Fantasy", 37 | }, 38 | { 39 | id: 36, 40 | name: "History", 41 | }, 42 | { 43 | id: 27, 44 | name: "Horror", 45 | }, 46 | { 47 | id: 10402, 48 | name: "Music", 49 | }, 50 | { 51 | id: 9648, 52 | name: "Mystery", 53 | }, 54 | { 55 | id: 10749, 56 | name: "Romance", 57 | }, 58 | { 59 | id: 878, 60 | name: "Science Fiction", 61 | }, 62 | { 63 | id: 10770, 64 | name: "TV Movie", 65 | }, 66 | { 67 | id: 53, 68 | name: "Thriller", 69 | }, 70 | { 71 | id: 10752, 72 | name: "War", 73 | }, 74 | { 75 | id: 37, 76 | name: "Western", 77 | }, 78 | ]; 79 | 80 | export const TMDB_TV_GENRES = [ 81 | { 82 | id: 10759, 83 | name: "Action & Adventure", 84 | }, 85 | { 86 | id: 16, 87 | name: "Animation", 88 | }, 89 | { 90 | id: 35, 91 | name: "Comedy", 92 | }, 93 | { 94 | id: 80, 95 | name: "Crime", 96 | }, 97 | { 98 | id: 99, 99 | name: "Documentary", 100 | }, 101 | { 102 | id: 18, 103 | name: "Drama", 104 | }, 105 | { 106 | id: 10751, 107 | name: "Family", 108 | }, 109 | { 110 | id: 10762, 111 | name: "Kids", 112 | }, 113 | { 114 | id: 9648, 115 | name: "Mystery", 116 | }, 117 | { 118 | id: 10763, 119 | name: "News", 120 | }, 121 | { 122 | id: 10764, 123 | name: "Reality", 124 | }, 125 | { 126 | id: 10765, 127 | name: "Sci-Fi & Fantasy", 128 | }, 129 | { 130 | id: 10766, 131 | name: "Soap", 132 | }, 133 | { 134 | id: 10767, 135 | name: "Talk", 136 | }, 137 | { 138 | id: 10768, 139 | name: "War & Politics", 140 | }, 141 | { 142 | id: 37, 143 | name: "Western", 144 | }, 145 | ]; 146 | -------------------------------------------------------------------------------- /src/main/api/tmdb/movie-details.types.ts: -------------------------------------------------------------------------------- 1 | export interface BelongsToCollection { 2 | id: number; 3 | name: string; 4 | poster_path: string; 5 | backdrop_path: string; 6 | } 7 | 8 | export interface Genre { 9 | id: number; 10 | name: string; 11 | } 12 | 13 | export interface ProductionCompany { 14 | id: number; 15 | logo_path: string; 16 | name: string; 17 | origin_country: string; 18 | } 19 | 20 | export interface ProductionCountry { 21 | iso_3166_1: string; 22 | name: string; 23 | } 24 | 25 | export interface SpokenLanguage { 26 | english_name: string; 27 | iso_639_1: string; 28 | name: string; 29 | } 30 | 31 | export interface Cast { 32 | adult: boolean; 33 | gender: number; 34 | id: number; 35 | known_for_department: string; 36 | name: string; 37 | original_name: string; 38 | popularity: number; 39 | profile_path: string; 40 | character: string; 41 | credit_id: string; 42 | order: number; 43 | } 44 | 45 | export interface Crew { 46 | adult: boolean; 47 | gender: number; 48 | id: number; 49 | known_for_department: string; 50 | name: string; 51 | original_name: string; 52 | popularity: number; 53 | profile_path: string; 54 | credit_id: string; 55 | department: string; 56 | job: string; 57 | } 58 | 59 | export interface Credits { 60 | id: number; 61 | cast: Cast[]; 62 | crew: Crew[]; 63 | } 64 | 65 | export interface MovieDetailsResults { 66 | adult: boolean; 67 | backdrop_path: string; 68 | belongs_to_collection: BelongsToCollection; 69 | budget: number; 70 | genres: Genre[]; 71 | homepage: string; 72 | id: number; 73 | imdb_id: string; 74 | original_language: string; 75 | original_title: string; 76 | overview: string; 77 | popularity: number; 78 | poster_path: string; 79 | production_companies: ProductionCompany[]; 80 | production_countries: ProductionCountry[]; 81 | release_date: string; 82 | revenue: number; 83 | runtime: number; 84 | spoken_languages: SpokenLanguage[]; 85 | status: string; 86 | tagline: string; 87 | title: string; 88 | video: boolean; 89 | vote_average: number; 90 | vote_count: number; 91 | credits: Credits; 92 | } 93 | -------------------------------------------------------------------------------- /src/main/api/tmdb/search.types.ts: -------------------------------------------------------------------------------- 1 | export interface KnownFor { 2 | backdrop_path: string; 3 | first_air_date: string; 4 | genre_ids: number[]; 5 | id: number; 6 | media_type: string; 7 | name: string; 8 | origin_country: string[]; 9 | original_language: string; 10 | original_name: string; 11 | overview: string; 12 | poster_path: string; 13 | vote_average: number; 14 | vote_count: number; 15 | } 16 | 17 | export interface Result { 18 | backdrop_path: string; 19 | first_air_date: string; 20 | genre_ids: number[]; 21 | id: number; 22 | media_type: string; 23 | name: string; 24 | origin_country: string[]; 25 | original_language: string; 26 | original_name: string; 27 | overview: string; 28 | popularity: number; 29 | poster_path: string; 30 | vote_average: number; 31 | vote_count: number; 32 | adult?: boolean; 33 | original_title: string; 34 | release_date: string; 35 | title: string; 36 | video?: boolean; 37 | gender?: number; 38 | known_for: KnownFor[]; 39 | known_for_department: string; 40 | profile_path?: unknown; 41 | } 42 | 43 | export interface SearchResponse { 44 | page: number; 45 | results: Result[]; 46 | total_pages: number; 47 | total_results: number; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/api/tmdb/sort-by.ts: -------------------------------------------------------------------------------- 1 | export const TMBD_SORT_BY = [ 2 | { 3 | value: "popularity.desc", 4 | label: "Popularity Descending", 5 | }, 6 | { 7 | value: "popularity.asc", 8 | label: "Popularity Ascending", 9 | }, 10 | { 11 | value: "release_date.desc", 12 | label: "Release Date Descending", 13 | }, 14 | { 15 | value: "release_date.asc", 16 | label: "Release Date Ascending", 17 | }, 18 | { 19 | value: "revenue.desc", 20 | label: "Revenue Descending", 21 | }, 22 | { 23 | value: "revenue.asc", 24 | label: "Revenue Ascending", 25 | }, 26 | { 27 | value: "primary_release_date.desc", 28 | label: "Primary Release Date Descending", 29 | }, 30 | { 31 | value: "primary_release_date.asc", 32 | label: "Primary Release Date Ascending", 33 | }, 34 | { 35 | value: "original_title.desc", 36 | label: "Original Title Descending", 37 | }, 38 | { 39 | value: "original_title.asc", 40 | label: "Original Title Ascending", 41 | }, 42 | { 43 | value: "vote_average.desc", 44 | label: "Vote Average Descending", 45 | }, 46 | { 47 | value: "vote_average.asc", 48 | label: "Vote Average Ascending", 49 | }, 50 | { 51 | value: "vote_count.desc", 52 | label: "Vote Count Descending", 53 | }, 54 | { 55 | value: "vote_count.asc", 56 | label: "Vote Count Ascending", 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /src/main/api/tmdb/tvshow-details.types.ts: -------------------------------------------------------------------------------- 1 | export interface CreatedBy { 2 | id: number; 3 | credit_id: string; 4 | name: string; 5 | gender: number; 6 | profile_path?: unknown; 7 | } 8 | 9 | export interface Genre { 10 | id: number; 11 | name: string; 12 | } 13 | 14 | export interface LastEpisodeToAir { 15 | air_date: string; 16 | episode_number: number; 17 | id: number; 18 | name: string; 19 | overview: string; 20 | production_code: string; 21 | runtime: number; 22 | season_number: number; 23 | show_id: number; 24 | still_path: string; 25 | vote_average: number; 26 | vote_count: number; 27 | } 28 | 29 | export interface Network { 30 | id: number; 31 | name: string; 32 | logo_path: string; 33 | origin_country: string; 34 | } 35 | 36 | export interface ProductionCompany { 37 | id: number; 38 | logo_path?: unknown; 39 | name: string; 40 | origin_country: string; 41 | } 42 | 43 | export interface Season { 44 | air_date: string; 45 | episode_count: number; 46 | id: number; 47 | name: string; 48 | overview: string; 49 | poster_path: string; 50 | season_number: number; 51 | } 52 | 53 | export interface SpokenLanguage { 54 | english_name: string; 55 | iso_639_1: string; 56 | name: string; 57 | } 58 | 59 | export interface ExternalIds { 60 | imdb_id: string; 61 | freebase_mid: string; 62 | freebase_id: string; 63 | tvdb_id: number; 64 | tvrage_id: number; 65 | wikidata_id: string; 66 | facebook_id: string; 67 | instagram_id: string; 68 | twitter_id: string; 69 | } 70 | 71 | export interface Cast { 72 | adult: boolean; 73 | gender: number; 74 | id: number; 75 | known_for_department: string; 76 | name: string; 77 | original_name: string; 78 | popularity: number; 79 | profile_path: string; 80 | character: string; 81 | credit_id: string; 82 | order: number; 83 | } 84 | 85 | export interface Crew { 86 | adult: boolean; 87 | gender: number; 88 | id: number; 89 | known_for_department: string; 90 | name: string; 91 | original_name: string; 92 | popularity: number; 93 | profile_path: string; 94 | credit_id: string; 95 | department: string; 96 | job: string; 97 | } 98 | 99 | export interface Credits { 100 | id: number; 101 | cast: Cast[]; 102 | crew: Crew[]; 103 | } 104 | 105 | export interface TvShowDetailsResults { 106 | adult: boolean; 107 | backdrop_path: string; 108 | created_by: CreatedBy[]; 109 | episode_run_time: number[]; 110 | first_air_date: string; 111 | genres: Genre[]; 112 | homepage: string; 113 | id: number; 114 | in_production: boolean; 115 | languages: string[]; 116 | last_air_date: string; 117 | last_episode_to_air: LastEpisodeToAir; 118 | name: string; 119 | next_episode_to_air?: unknown; 120 | networks: Network[]; 121 | number_of_episodes: number; 122 | number_of_seasons: number; 123 | origin_country: string[]; 124 | original_language: string; 125 | original_name: string; 126 | overview: string; 127 | popularity: number; 128 | poster_path: string; 129 | production_companies: ProductionCompany[]; 130 | production_countries: unknown[]; 131 | seasons: Season[]; 132 | spoken_languages: SpokenLanguage[]; 133 | status: string; 134 | tagline: string; 135 | type: string; 136 | vote_average: number; 137 | vote_count: number; 138 | external_ids: ExternalIds; 139 | credits: Credits; 140 | } 141 | -------------------------------------------------------------------------------- /src/main/api/trpc-client.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | 3 | export const t = initTRPC.create({ isServer: true }); 4 | -------------------------------------------------------------------------------- /src/main/api/updater.router.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from "electron-updater"; 2 | import { t } from "./trpc-client"; 3 | 4 | export const updaterRouter = t.router({ 5 | quitAndInstall: t.procedure.mutation(async () => { 6 | autoUpdater.quitAndInstall(false, true); 7 | }), 8 | }); 9 | -------------------------------------------------------------------------------- /src/main/api/vidsrc.router.ts: -------------------------------------------------------------------------------- 1 | import z from "zod"; 2 | import FormData from "form-data"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { t } from "./trpc-client"; 5 | 6 | export const vidSrcRouter = t.router({ 7 | getSubUrl: t.procedure 8 | .input( 9 | z.object({ 10 | url: z.string(), 11 | }), 12 | ) 13 | .mutation(async ({ input }) => { 14 | const gzippedRes = await axiosInstance.get(input.url, { 15 | headers: { 16 | "Accept-Encoding": "gzip, deflate, br", 17 | "Accept-Language": "en-US,en;q=0.5", 18 | TE: "trailers", 19 | }, 20 | responseType: "arraybuffer", 21 | }); 22 | const gzipId = input.url.split("/").pop()?.replace(".gz", ""); 23 | 24 | const formData = new FormData(); 25 | formData.append("sub_data", gzippedRes.data, { 26 | filename: "blob", 27 | contentType: "application/octet-stream", 28 | }); 29 | formData.append("sub_id", gzipId); 30 | formData.append("sub_enc", "UTF-8"); 31 | formData.append("sub_src", "ops"); 32 | formData.append("subformat", "srt"); 33 | 34 | const subUrl = await axiosInstance.post( 35 | "https://vidsrc.stream/get_sub_url", 36 | formData, 37 | { 38 | headers: { 39 | "Content-Type": 40 | "multipart/form-data; boundary=---------------------------17099936243183683645642750180", 41 | "Accept-Encoding": "gzip, deflate, br", 42 | "Accept-Language": "en-US,en;q=0.5", 43 | Origin: "https://vidsrc.stream", 44 | Referer: `https://vidsrc.stream/`, 45 | "X-Requested-With": "XMLHttpRequest", 46 | }, 47 | }, 48 | ); 49 | 50 | return `https://vidsrc.stream${subUrl.data}`; 51 | }), 52 | }); 53 | -------------------------------------------------------------------------------- /src/main/api/vlc.router.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { launchVlc, stopVlc } from "../lib/vlc"; 3 | import { t } from "./trpc-client"; 4 | 5 | export const vlcRouter = t.router({ 6 | launch: t.procedure 7 | .input( 8 | z.object({ 9 | url: z.string(), 10 | }), 11 | ) 12 | .mutation(({ input }) => { 13 | launchVlc(input.url); 14 | }), 15 | quit: t.procedure.mutation(() => { 16 | stopVlc(); 17 | }), 18 | }); 19 | -------------------------------------------------------------------------------- /src/main/extractors/2embed.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { Source } from "types/sources"; 3 | import { ContentType } from "types/tmbd"; 4 | import log from "electron-log"; 5 | import { axiosInstance } from "../utils/axios"; 6 | import { IExtractor } from "./types"; 7 | import { formatToJSON, getResolutionFromM3u8 } from "./utils"; 8 | 9 | export class TwoEmbedExtractor implements IExtractor { 10 | name = "2Embed"; 11 | 12 | logger = log.scope(this.name); 13 | 14 | url: string = "https://www.2embed.cc/"; 15 | 16 | referer: string = "https://www.2embed.cc/"; 17 | 18 | async extractUrls( 19 | imdbId: string, 20 | type: ContentType, 21 | season?: number, 22 | episode?: number, 23 | ): Promise { 24 | try { 25 | const url = 26 | // eslint-disable-next-line no-nested-ternary 27 | type === "movie" 28 | ? `${this.url}embed/${imdbId}` 29 | : type === "tv" 30 | ? `${this.url}embedtv${imdbId}&s=${season}&e=${episode}/` 31 | : ""; 32 | 33 | let res = await axiosInstance.get(url); 34 | const $ = load(res.data); 35 | const iframeUrl = $("iframe").attr("data-src"); 36 | const id = iframeUrl?.match(/\?id=(.*?)&/)?.[1]; 37 | if (!id) throw new Error("No id found"); 38 | 39 | res = await axiosInstance.get(`https://wishfast.top/e/${id}`, { 40 | headers: { 41 | referer: this.referer, 42 | }, 43 | }); 44 | this.logger.debug( 45 | formatToJSON(res.data.match(/sources:\s*(\[.*?\])/)[1]), 46 | formatToJSON(res.data.match(/tracks:\s*(\[.*?\])/)[1]), 47 | ); 48 | const sources = JSON.parse( 49 | formatToJSON(res.data.match(/sources:\s*(\[.*?\])/)[1]), 50 | ); 51 | const tracks = JSON.parse( 52 | formatToJSON(res.data.match(/tracks:\s*(\[.*?\])/)[1]), 53 | ); 54 | const quality = await getResolutionFromM3u8(sources[0].file, true); 55 | let thumbnails = tracks.find((t: any) => t.kind === "thumbnails").file; 56 | if (thumbnails) { 57 | thumbnails = `https://wishfast.top${thumbnails}`; 58 | } 59 | const thumbnailContent = await axiosInstance.get(thumbnails, { 60 | headers: { 61 | referer: "https://wishfast.top/", 62 | }, 63 | }); 64 | const subtitles = tracks 65 | .filter((t: any) => t.kind === "captions") 66 | .map((subtitle: any) => ({ 67 | file: subtitle.file, 68 | label: subtitle.label, 69 | kind: "captions", 70 | })); 71 | 72 | return [ 73 | { 74 | server: this.name, 75 | quality, 76 | source: { 77 | url: sources[0].file, 78 | }, 79 | type: "m3u8", 80 | thumbnails: { 81 | url: thumbnailContent.data, 82 | requiresBlob: true, 83 | }, 84 | subtitles, 85 | }, 86 | ]; 87 | } catch (error) { 88 | if (error instanceof Error) this.logger.error(error.message); 89 | return []; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/extractors/closeload.ts: -------------------------------------------------------------------------------- 1 | import { Source, Subtitle } from "types/sources"; 2 | import log from "electron-log"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { IExtractor } from "./types"; 5 | import { load } from "cheerio"; 6 | import { getResolutionFromM3u8 } from "./utils"; 7 | import vm from "vm"; 8 | 9 | export class CloseloadExtractor implements IExtractor { 10 | name = "Closeload"; 11 | 12 | logger = log.scope(this.name); 13 | 14 | url = "https://closeload.top"; 15 | 16 | async extractUrl(url: string): Promise { 17 | try { 18 | const baseUrl = new URL(url).origin; 19 | const iframeRes = await axiosInstance.get(url, { 20 | headers: { 21 | Referer: "https://ridomovies.tv/", 22 | }, 23 | }); 24 | const iframeRes$ = load(iframeRes.data); 25 | const subtitles: Subtitle[] = iframeRes$("track") 26 | .map((_, el) => { 27 | const track = iframeRes$(el); 28 | return { 29 | file: `${baseUrl}${track.attr("src")}`, 30 | label: track.attr("label")!, 31 | kind: "subtitles", 32 | }; 33 | }) 34 | .get(); 35 | 36 | const evalCode = iframeRes$("script") 37 | .filter((_, el) => { 38 | const script = iframeRes$(el); 39 | return (script.attr("type") === "text/javascript" && 40 | script.html()?.includes("eval"))!; 41 | }) 42 | .html(); 43 | if (!evalCode) throw new Error("No eval code found"); 44 | 45 | let sourceUrl = ""; 46 | 47 | const sandbox = { 48 | $: () => ({ 49 | ready: (callback: any) => callback(), 50 | text: () => {}, 51 | on: () => {}, 52 | attr: () => {}, 53 | prepend: () => {}, 54 | }), 55 | videojs: () => { 56 | const player = { 57 | src: () => {}, 58 | ready: (readyCallback: any) => readyCallback(), 59 | hotkeys: (config: any) => {}, 60 | one: () => {}, 61 | on: () => {}, 62 | getChild: () => ({ 63 | addChild: () => {}, 64 | }), 65 | textTracks: () => ({ 66 | on: () => {}, 67 | }), 68 | }; 69 | return player; 70 | }, 71 | atob: (input: string) => { 72 | sourceUrl = Buffer.from(input, "base64").toString("utf-8"); 73 | return sourceUrl; 74 | }, 75 | document: { 76 | ready: (callback: any) => callback(), 77 | }, 78 | console, 79 | hotkeys: () => {}, 80 | } as any; 81 | 82 | sandbox.videojs.Vhs = { 83 | GOAL_BUFFER_LENGTH: 0, 84 | MAX_GOAL_BUFFER_LENGTH: 0, 85 | }; 86 | sandbox.videojs.getComponent = () => {}; 87 | sandbox.videojs.registerComponent = () => {}; 88 | sandbox.videojs.extend = () => {}; 89 | sandbox.videojs.getChild = () => {}; 90 | 91 | const context = vm.createContext(sandbox); 92 | vm.runInContext(evalCode, context); 93 | 94 | if (!sourceUrl) throw new Error("No source found"); 95 | 96 | const quality = await getResolutionFromM3u8(sourceUrl, true, { 97 | Referer: this.url, 98 | }); 99 | return { 100 | server: this.name, 101 | quality, 102 | type: "m3u8", 103 | source: { 104 | url: sourceUrl, 105 | }, 106 | subtitles, 107 | proxySettings: { 108 | type: "m3u8", 109 | referer: this.url + "/", 110 | origin: this.url, 111 | }, 112 | }; 113 | } catch (error) { 114 | if (error instanceof Error) this.logger.error(error.message); 115 | return undefined; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/extractors/embedsito.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "types/sources"; 2 | import log from "electron-log"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { IExtractor } from "./types"; 5 | 6 | export class EmbedsitoExtractor implements IExtractor { 7 | logger = log.scope("Embedsito"); 8 | 9 | url: string = "https://embedsito.com/api/source/"; 10 | 11 | async extractUrl(url: string): Promise { 12 | try { 13 | const res = await axiosInstance.post( 14 | `https://embedsito.com/api/source/${url}`, 15 | ); 16 | 17 | const file = res.data.data[res.data.data.length - 1]; 18 | const redirectUrl = file.file; 19 | const quality = file.label; 20 | const fileType = file.type; 21 | 22 | const finalUrl = await axiosInstance.get(redirectUrl, { 23 | maxRedirects: 0, 24 | validateStatus: (status) => { 25 | return status >= 200 && status < 400; 26 | }, 27 | }); 28 | return { 29 | server: "Embedsito", 30 | source: { 31 | url: finalUrl.headers.location!, 32 | }, 33 | type: fileType === "mp4" ? "mp4" : "m3u8", 34 | quality, 35 | }; 36 | } catch (error) { 37 | if (error instanceof Error) this.logger.error(error.message); 38 | return undefined; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/extractors/filemoon.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import log from "electron-log"; 3 | import { Source } from "types/sources"; 4 | import vm from "vm"; 5 | import { axiosInstance } from "../utils/axios"; 6 | import { IExtractor } from "./types"; 7 | import { getResolutionFromM3u8 } from "./utils"; 8 | 9 | export class FileMoonExtractor implements IExtractor { 10 | logger = log.scope("FileMoon"); 11 | 12 | url = "https://filemoon.sx/"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url); 17 | const regex = /eval\((.*)\)/g; 18 | const evalCode = regex.exec(res.data)?.[0]; 19 | if (!evalCode) throw new Error("No eval code found"); 20 | 21 | const extractSource = async (file: string): Promise => { 22 | const quality = await getResolutionFromM3u8(file, true); 23 | 24 | return { 25 | server: "FileMoon", 26 | source: { 27 | url: file, 28 | }, 29 | type: file.includes(".m3u8") ? "m3u8" : "mp4", 30 | quality, 31 | }; 32 | }; 33 | 34 | const extractionPromise = new Promise((resolve, reject) => { 35 | const sandbox = { 36 | jwplayer: () => ({ 37 | setup: async (config: any) => { 38 | if (config.sources && Array.isArray(config.sources)) { 39 | const firstSource = config.sources[0]; 40 | if (firstSource && firstSource.file) { 41 | resolve(extractSource(firstSource.file)); 42 | } else { 43 | reject(new Error("No file found")); 44 | } 45 | } else { 46 | reject(new Error("No sources found")); 47 | } 48 | }, 49 | on: () => {}, 50 | addButton: () => {}, 51 | getButton: () => {}, 52 | seek: () => {}, 53 | getPosition: () => {}, 54 | addEventListener: () => {}, 55 | setCurrentCaptions: () => {}, 56 | pause: () => {}, 57 | }), 58 | document: { 59 | addEventListener: (event: string, callback: () => void) => { 60 | if (event === "DOMContentLoaded") { 61 | callback(); 62 | } 63 | }, 64 | }, 65 | fetch: async () => ({ 66 | json: async () => ({}), 67 | }), 68 | $: () => ({ 69 | hide: () => {}, 70 | get: () => {}, 71 | detach: () => ({ 72 | insertAfter: () => {}, 73 | }), 74 | }), 75 | p2pml: { 76 | hlsjs: { 77 | Engine: class { 78 | constructor() { 79 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 80 | // @ts-ignore 81 | this.on = () => {}; 82 | } 83 | 84 | createLoaderClass() {} 85 | }, 86 | }, 87 | }, 88 | }; 89 | 90 | vm.createContext(sandbox); 91 | vm.runInContext(evalCode, sandbox); 92 | }); 93 | 94 | return await extractionPromise; 95 | } catch (error) { 96 | if (error instanceof Error) this.logger.error(error.message); 97 | return undefined; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/extractors/moviesapi.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import log from "electron-log"; 3 | import { ContentType } from "types/tmbd"; 4 | import crypto from "crypto"; 5 | import { Source } from "types/sources"; 6 | import vm from "vm"; 7 | import { IExtractor } from "./types"; 8 | import { getResolutionFromM3u8 } from "./utils"; 9 | import { axiosInstance } from "../utils/axios"; 10 | import { CryptoJSAesJson } from "../utils/crypto"; 11 | 12 | export class MoviesApiExtractor implements IExtractor { 13 | name = "MoviesApi"; 14 | 15 | logger = log.scope(this.name); 16 | 17 | url = "https://moviesapi.club/"; 18 | 19 | referer = "https://w1.moviesapi.club/"; 20 | 21 | private getKey(stringData: string) { 22 | const sandbox = { 23 | JScripts: "", 24 | CryptoJSAesJson: { 25 | decrypt: (data: string, key: string) => { 26 | return JSON.stringify(key); 27 | }, 28 | }, 29 | }; 30 | vm.createContext(sandbox); 31 | const key = vm.runInContext(stringData, sandbox); 32 | return key; 33 | } 34 | 35 | async extractUrls( 36 | tmdbId: string, 37 | type: ContentType, 38 | season?: number, 39 | episode?: number, 40 | ): Promise { 41 | try { 42 | const url = 43 | type === "movie" 44 | ? `${this.url}movie/${tmdbId}` 45 | : `${this.url}tv/${tmdbId}-${season}-${episode}`; 46 | 47 | const res = await axiosInstance.get(url, { 48 | headers: { 49 | referer: this.referer, 50 | }, 51 | }); 52 | const res$ = load(res.data); 53 | const iframeUrl = res$("iframe").attr("src"); 54 | this.logger.debug(iframeUrl); 55 | 56 | if (!iframeUrl) throw new Error("No iframe url found"); 57 | 58 | const res2 = await axiosInstance.get(iframeUrl, { 59 | headers: { 60 | referer: this.referer, 61 | }, 62 | }); 63 | const res2$ = load(res2.data); 64 | const stringData = res2$("body script").eq(2).html(); 65 | if (!stringData) throw new Error("No script found"); 66 | const key = this.getKey(stringData); 67 | this.logger.debug(key); 68 | 69 | const regex = /JScripts\s*=\s*'([^']*)'/; 70 | const base64EncryptedData = regex.exec(res2.data)![1]; 71 | 72 | const decryptedString = CryptoJSAesJson.decrypt(base64EncryptedData, key); 73 | 74 | const sources = JSON.parse( 75 | decryptedString.match(/sources: ([^\]]*\])/)![1], 76 | ); 77 | const tracks = JSON.parse( 78 | decryptedString.match(/tracks: ([^]*?\}\])/)![1], 79 | ); 80 | 81 | const subtitles = tracks.filter((it: any) => it.kind === "captions"); 82 | const thumbnails = tracks.filter((it: any) => it.kind === "thumbnails"); 83 | 84 | const highestQuality = await getResolutionFromM3u8(sources[0].file, true); 85 | 86 | return [ 87 | { 88 | server: this.name, 89 | source: { 90 | url: sources[0].file, 91 | }, 92 | type: sources[0].type === "hls" ? "m3u8" : "mp4", 93 | quality: highestQuality, 94 | subtitles: subtitles.map((it: any) => ({ 95 | file: it.file, 96 | label: it.label, 97 | kind: it.kind, 98 | })), 99 | thumbnails: { 100 | url: thumbnails[0]?.file, 101 | }, 102 | proxySettings: { 103 | type: "m3u8", 104 | referer: this.referer, 105 | }, 106 | }, 107 | ]; 108 | } catch (err) { 109 | if (err instanceof Error) this.logger.error(err.message); 110 | return []; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/extractors/multimovies.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "types/sources"; 2 | import log from "electron-log"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { IExtractor } from "./types"; 5 | import { ContentType } from "types/tmbd"; 6 | import { load } from "cheerio"; 7 | import { StreamWishExtractor } from "./streamwish"; 8 | 9 | export class MultiMoviesExtractor implements IExtractor { 10 | name = "MultiMovies"; 11 | 12 | logger = log.scope(this.name); 13 | 14 | url = "https://multinews.tech"; 15 | 16 | private streamWishExtractor = new StreamWishExtractor(); 17 | 18 | async extractUrls( 19 | showName: string, 20 | type: ContentType, 21 | season?: number, 22 | episode?: number, 23 | ): Promise { 24 | try { 25 | const searchResult = await axiosInstance.get( 26 | `${this.url}/wp-json/dooplay/search?keyword=${encodeURIComponent( 27 | showName, 28 | )}&nonce=6fffa73dee`, 29 | ); 30 | const show = searchResult.data[Object.keys(searchResult.data)[0]]; 31 | let showUrl = show.url; 32 | if (!show) throw new Error("No show found"); 33 | if (type === "tv") { 34 | this.logger.debug(show.url); 35 | const slug = show.url.split("/tvshows/")[1].split("/")[0]; 36 | showUrl = `${this.url}/episodes/${slug}-${season}x${episode}`; 37 | } 38 | 39 | const showPageResult = await axiosInstance.get(showUrl); 40 | const showPageResult$ = load(showPageResult.data); 41 | const iframeUrl = showPageResult$("#source-player-1") 42 | .children() 43 | .find("iframe") 44 | .attr("src"); 45 | const sourceUrl = await this.streamWishExtractor.extractUrl( 46 | iframeUrl!, 47 | this.name, 48 | ); 49 | if (!sourceUrl) throw new Error("No source found"); 50 | return [sourceUrl]; 51 | } catch (error) { 52 | if (error instanceof Error) this.logger.error(error.message); 53 | return []; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/extractors/myfilestorage.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { ContentType } from "types/tmbd"; 4 | import { axiosInstance } from "../utils/axios"; 5 | import { IExtractor } from "./types"; 6 | import { addLeadingZero } from "./utils"; 7 | 8 | export class MyFileStorageExtractor implements IExtractor { 9 | name = "MyFileStorage"; 10 | 11 | logger = log.scope("MyFileStorage"); 12 | 13 | url = "https://myfilestorage.xyz"; 14 | 15 | referer = "https://bflix.gs/"; 16 | 17 | async extractUrls( 18 | tmdbId: string, 19 | type: ContentType, 20 | season?: number, 21 | episode?: number, 22 | ): Promise { 23 | try { 24 | let url = `${this.url}/${tmdbId}.mp4`; 25 | 26 | if (type === "tv" && season && episode) { 27 | url = `${this.url}/tv/${tmdbId}/s${season}e${addLeadingZero( 28 | episode, 29 | )}.mp4`; 30 | } 31 | 32 | const res = await axiosInstance.head(url, { 33 | validateStatus: () => true, 34 | headers: { 35 | referer: this.referer, 36 | }, 37 | }); 38 | this.logger.debug(res.status, res.statusText); 39 | if (res.status !== 200) throw new Error("No sources found"); 40 | 41 | return [ 42 | { 43 | server: this.name, 44 | source: { 45 | url, 46 | }, 47 | type: "mp4", 48 | quality: "720p/1080p", 49 | proxySettings: { 50 | type: "mp4", 51 | referer: this.referer, 52 | }, 53 | }, 54 | ]; 55 | } catch (e) { 56 | if (e instanceof Error) this.logger.error(e.message); 57 | return []; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/extractors/rabbitstream.ts: -------------------------------------------------------------------------------- 1 | import * as m3u8Parser from "m3u8-parser"; 2 | import crypto from "crypto"; 3 | import { Source } from "types/sources"; 4 | import log from "electron-log"; 5 | import { axiosInstance } from "../utils/axios"; 6 | import { getResolutionName } from "./utils"; 7 | import { IExtractor } from "./types"; 8 | 9 | export class RabbitStreamExtractor implements IExtractor { 10 | logger = log.scope("VidCloud"); 11 | 12 | url: string = "https://rabbitstream.net/"; 13 | 14 | referer: string = "https://rabbitstream.net/"; 15 | 16 | private decryptionKeyUrl = 17 | "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"; 18 | 19 | private md5(input: Buffer): Buffer { 20 | return crypto.createHash("md5").update(input).digest(); 21 | } 22 | 23 | private generateKey(salt: Buffer, secret: Buffer): Buffer { 24 | let key = this.md5(Buffer.concat([secret, salt])); 25 | let currentKey = key; 26 | while (currentKey.length < 48) { 27 | key = this.md5(Buffer.concat([key, secret, salt])); 28 | currentKey = Buffer.concat([currentKey, key]); 29 | } 30 | return currentKey; 31 | } 32 | 33 | private decryptSourceUrl(decryptionKey: Buffer, sourceUrl: string): string { 34 | const cipherData = Buffer.from(sourceUrl, "base64"); 35 | const encrypted = cipherData.slice(16); 36 | const algorithm = "aes-256-cbc"; 37 | const iv = decryptionKey.slice(32); 38 | const decryptionKeyWithoutIv = decryptionKey.slice(0, 32); 39 | const decipher = crypto.createDecipheriv( 40 | algorithm, 41 | decryptionKeyWithoutIv, 42 | iv, 43 | ); 44 | const decryptedData = Buffer.concat([ 45 | decipher.update(encrypted), 46 | decipher.final(), 47 | ]); 48 | return decryptedData.toString("utf8"); 49 | } 50 | 51 | private decrypt(input: string, key: string): string { 52 | const decryptionKey = this.generateKey( 53 | Buffer.from(input, "base64").slice(8, 16), 54 | Buffer.from(key, "utf8"), 55 | ); 56 | return this.decryptSourceUrl(decryptionKey, input); 57 | } 58 | 59 | private async getDecryptionKey() { 60 | const res = await axiosInstance.get(this.decryptionKeyUrl); 61 | return res.data; 62 | } 63 | 64 | private async extractSourceUrl(url: string) { 65 | const id = url.split("/").pop()!.split("?")[0]; 66 | const apiUrl = `${this.url}ajax/embed-5/getSources?id=${id}`; 67 | const res = await axiosInstance.get(apiUrl); 68 | 69 | const sources = res.data.sources; 70 | const subtitles = res.data.tracks; 71 | 72 | const isDecrypted = !sources.includes("https://"); 73 | 74 | let source = null; 75 | if (isDecrypted) { 76 | const decryptionKey = await this.getDecryptionKey(); 77 | const decryptedSourceUrl = this.decrypt(sources, decryptionKey); 78 | const json = JSON.parse(decryptedSourceUrl); 79 | source = json[0]; 80 | } else { 81 | source = sources[0]; 82 | } 83 | 84 | return { 85 | sourceUrl: source.file, 86 | subtitles, 87 | isHls: source.type === "hls", 88 | }; 89 | } 90 | 91 | async extractUrl(url: string): Promise { 92 | try { 93 | const { sourceUrl, subtitles, isHls } = await this.extractSourceUrl(url); 94 | 95 | const m3u8Manifest = await axiosInstance.get(sourceUrl); 96 | 97 | const parser = new m3u8Parser.Parser(); 98 | parser.push(m3u8Manifest.data); 99 | parser.end(); 100 | 101 | const parsedManifest = parser.manifest; 102 | const highestQuality = parsedManifest.playlists.reduce( 103 | (prev: any, current: any) => { 104 | return prev.attributes.BANDWIDTH > current.attributes.BANDWIDTH 105 | ? prev 106 | : current; 107 | }, 108 | ); 109 | 110 | return { 111 | server: "VidCloud", 112 | source: { 113 | url: sourceUrl, 114 | }, 115 | type: isHls ? "m3u8" : "mp4", 116 | quality: getResolutionName(highestQuality.attributes.RESOLUTION.height), 117 | subtitles, 118 | }; 119 | } catch (error) { 120 | if (error instanceof Error) { 121 | this.logger.error(error.message); 122 | } 123 | return undefined; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/extractors/remotestream.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { ContentType } from "types/tmbd"; 4 | import { axiosInstance } from "../utils/axios"; 5 | import { IExtractor } from "./types"; 6 | import { getResolutionFromM3u8 } from "./utils"; 7 | 8 | export class RemoteStreamExtractor implements IExtractor { 9 | name = "RemoteStream"; 10 | 11 | logger = log.scope(this.name); 12 | 13 | url = "https://remotestream.cc/e/?"; 14 | 15 | referer = "https://remotestre.am/"; 16 | 17 | private apiKey = "bRR3S48MbSnqjSaYNdCrBLfTIGQQNPRo"; 18 | 19 | async extractUrls( 20 | imdbId: string, 21 | type: ContentType, 22 | season?: number | undefined, 23 | episode?: number | undefined, 24 | ): Promise { 25 | try { 26 | const url = 27 | type === "movie" 28 | ? `${this.url}imdb=${imdbId}&apikey=${this.apiKey}` 29 | : `${this.url}imdb=${imdbId}&s=${season}&e=${episode}&apikey=${this.apiKey}`; 30 | const res = await axiosInstance.get(url); 31 | 32 | const fileRegex = /"file":"(.*?)"/; 33 | const match = res.data.match(fileRegex); 34 | 35 | if (!match || !match[1]) throw new Error("No match found"); 36 | 37 | this.logger.debug(match[1]); 38 | const quality = await getResolutionFromM3u8(match[1], true, { 39 | referer: this.referer, 40 | }); 41 | 42 | return [ 43 | { 44 | server: this.name, 45 | source: { 46 | url: match[1], 47 | }, 48 | type: "m3u8", 49 | quality, 50 | proxySettings: { 51 | type: "m3u8", 52 | origin: this.referer, 53 | referer: this.referer, 54 | }, 55 | }, 56 | ]; 57 | } catch (error) { 58 | if (error instanceof Error) this.logger.error(error.message); 59 | return []; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/cf.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | 6 | export class SmashyCfExtractor implements IExtractor { 7 | name = "Smashy (CF)"; 8 | 9 | logger = log.scope(this.name); 10 | 11 | url = "https://embed.smashystream.com/cf.php"; 12 | 13 | async extractUrl(url: string): Promise { 14 | try { 15 | const res = await axiosInstance.get(url, { 16 | headers: { 17 | referer: url, 18 | }, 19 | }); 20 | 21 | const file = res.data.match(/file:\s*"([^"]+)"/)[1]; 22 | 23 | const fileRes = await axiosInstance.head(file); 24 | 25 | if (fileRes.status !== 200 || fileRes.data.includes("404")) 26 | return undefined; 27 | 28 | return { 29 | server: this.name, 30 | source: { 31 | url: file, 32 | }, 33 | type: file.includes(".m3u8") ? "m3u8" : "mp4", 34 | quality: "Unknown", 35 | }; 36 | } catch (err) { 37 | if (err instanceof Error) this.logger.error(err.message); 38 | return undefined; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/dudmovie.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyDudMovieExtractor implements IExtractor { 8 | name = "Smashy (DM)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/dud_movie.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/new\s+Playerjs\((\{[^]*?\})\);/)[1].replace(/'/g, '"'), 23 | ); 24 | const fileUrl = config.file.find( 25 | (it: any) => it.title === "English", 26 | ).file; 27 | 28 | const quality = await getResolutionFromM3u8(fileUrl, true); 29 | return { 30 | server: this.name, 31 | source: { 32 | url: fileUrl, 33 | }, 34 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 35 | quality, 36 | }; 37 | } catch (err) { 38 | if (err instanceof Error) this.logger.error(err.message); 39 | return undefined; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/dued.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import log from "electron-log"; 3 | import { Source } from "types/sources"; 4 | import { axiosInstance } from "../../utils/axios"; 5 | import { IExtractor } from "../types"; 6 | import { getResolutionFromM3u8 } from "../utils"; 7 | 8 | export class SmashyDuedExtractor implements IExtractor { 9 | name = "Smashy (D)"; 10 | 11 | logger = log.scope(this.name); 12 | 13 | url = "https://embed.smashystream.com/dued.php"; 14 | 15 | async extractUrl(url: string): Promise { 16 | try { 17 | const res = await axiosInstance.get(url, { 18 | headers: { 19 | referer: url, 20 | }, 21 | }); 22 | const res$ = load(res.data); 23 | const iframeUrl = res$("iframe").attr("src"); 24 | if (!iframeUrl) throw new Error("No iframe found"); 25 | const mainUrl = new URL(iframeUrl); 26 | const iframeRes = await axiosInstance.get(iframeUrl!, { 27 | headers: { 28 | referer: url, 29 | }, 30 | }); 31 | const urlTextFile = `${mainUrl.origin}${ 32 | iframeRes.data.match(/"file":"([^"]+)"/)[1] 33 | }`; 34 | const csrfToken = iframeRes.data.match(/"key":"([^"]+)"/)[1]; 35 | const textRes = await axiosInstance.post(urlTextFile, null, { 36 | headers: { 37 | "Content-Type": "application/x-www-form-urlencoded", 38 | "X-CSRF-TOKEN": csrfToken, 39 | Referer: iframeUrl, 40 | }, 41 | }); 42 | const textFilePlaylist = textRes.data.find( 43 | (item: any) => item.title === "English", 44 | ).file; 45 | const textFilePlaylistRes = await axiosInstance.post( 46 | `${mainUrl.origin}/playlist/${textFilePlaylist.slice(1)}.txt`, 47 | null, 48 | { 49 | headers: { 50 | "Content-Type": "application/x-www-form-urlencoded", 51 | "X-CSRF-TOKEN": csrfToken, 52 | Referer: iframeUrl, 53 | }, 54 | }, 55 | ); 56 | const quality = await getResolutionFromM3u8( 57 | textFilePlaylistRes.data, 58 | true, 59 | ); 60 | return { 61 | server: this.name, 62 | source: { 63 | url: textFilePlaylistRes.data, 64 | }, 65 | quality, 66 | type: "m3u8", 67 | }; 68 | } catch (err) { 69 | if (err instanceof Error) this.logger.error(err.message); 70 | return undefined; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/ee.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | 6 | export class SmashyEeMovieExtractor implements IExtractor { 7 | name = "Smashy (EE)"; 8 | 9 | logger = log.scope(this.name); 10 | 11 | url = "https://embed.smashystream.com/cf.php"; 12 | 13 | async extractUrl(url: string): Promise { 14 | try { 15 | const res = await axiosInstance.get(url, { 16 | headers: { 17 | referer: url, 18 | }, 19 | }); 20 | 21 | const file = res.data.match(/file:\s*"([^"]+)"/)[1]; 22 | 23 | if (file.includes("/404Found.mp4")) return undefined; 24 | 25 | const fileRes = await axiosInstance.head(file); 26 | if (fileRes.status !== 200 || fileRes.data.includes("404")) 27 | return undefined; 28 | 29 | return { 30 | server: this.name, 31 | source: { 32 | url: file, 33 | }, 34 | type: "mp4", 35 | quality: "Unknown", 36 | }; 37 | } catch (err) { 38 | if (err instanceof Error) this.logger.error(err.message); 39 | return undefined; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/ems.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyEmsExtractor implements IExtractor { 8 | name = "Smashy (EMS)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/ems.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/new\s+Playerjs\((\{[^]*?\})\);/)[1].replace(/'/g, '"'), 23 | ); 24 | const fileUrl = config.file; 25 | 26 | const subtitleArray = config.subtitle 27 | .split(",") 28 | .map((entry: any) => { 29 | const nameRegex = /\[([^\]]*)\]/; 30 | const urlRegex = /(https:\/\/[^\s,]+)/; 31 | const nameMatch = nameRegex.exec(entry); 32 | const urlMatch = urlRegex.exec(entry); 33 | const name = 34 | nameMatch && nameMatch[1].trim() 35 | ? nameMatch[1].trim() 36 | : urlMatch && urlMatch[1]; 37 | const subtitleUrl = urlMatch && urlMatch[0].trim(); 38 | return { 39 | file: subtitleUrl, 40 | label: name, 41 | kind: "captions", 42 | }; 43 | }) 44 | .filter((subtitle: any) => subtitle.file !== null); 45 | 46 | const hlsResponse = await axiosInstance.get(fileUrl, { 47 | headers: { 48 | referer: url, 49 | }, 50 | }); 51 | if (hlsResponse.data === "") return; 52 | 53 | const quality = await getResolutionFromM3u8(hlsResponse.data, false); 54 | return { 55 | server: this.name, 56 | source: { 57 | url: fileUrl, 58 | }, 59 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 60 | quality, 61 | subtitles: subtitleArray, 62 | }; 63 | } catch (err) { 64 | if (err instanceof Error) this.logger.error(err.message); 65 | return undefined; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/ffix.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionName } from "../utils"; 6 | 7 | export class SmashyFFixExtractor implements IExtractor { 8 | name = "Smashy (FFix)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/ffix1.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/var\s+config\s*=\s*({.*?});/)[1], 23 | ); 24 | const fileUrl = config.file.split(",")[0]; 25 | 26 | const vttArray = config.subtitle 27 | .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) 28 | .map((entry: any) => { 29 | const [, name, link] = entry.match( 30 | /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/, 31 | ); 32 | return { name, link: link.replace(",", "") }; 33 | }); 34 | 35 | return { 36 | server: this.name, 37 | source: { 38 | url: fileUrl.split("]")[1], 39 | }, 40 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 41 | quality: getResolutionName( 42 | parseInt(fileUrl.split("]")[0].split("[")[1], 10), 43 | ), 44 | subtitles: vttArray 45 | .filter((it: any) => !it.link.includes("thumbnails")) 46 | .map((subtitle: any) => ({ 47 | file: subtitle.link, 48 | label: subtitle.name, 49 | kind: "captions", 50 | })), 51 | thumbnails: { 52 | url: vttArray.find((it: any) => it.link.includes("thumbnails"))?.link, 53 | }, 54 | }; 55 | } catch (err) { 56 | if (err instanceof Error) this.logger.error(err.message); 57 | return undefined; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/fizzzz.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionName } from "../utils"; 6 | 7 | export class SmashyFizzzzExtractor implements IExtractor { 8 | name = "Smashy (Fiz)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/fizzzz1.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/new\s+Playerjs\((\{[^]*?\})\);/)[1].replace(/'/g, '"'), 23 | ); 24 | const fileUrl = config.file.split(",")[0].split("]")[1]; 25 | const quality = config.file.split(",")[0].split("]")[0].split("[")[1]; 26 | 27 | const subtitleArray = config.subtitle 28 | .split(",") 29 | .map((entry: any) => { 30 | const nameRegex = /\[([^\]]*)\]/; 31 | const urlRegex = /https:\/\/cc\.2cdns\.com\/.*?\/(\w+-\d+)\.vtt/; 32 | const nameMatch = nameRegex.exec(entry); 33 | const urlMatch = urlRegex.exec(entry); 34 | const name = 35 | nameMatch && nameMatch[1].trim() 36 | ? nameMatch[1].trim() 37 | : urlMatch && urlMatch[1]; 38 | const subtitleUrl = urlMatch && urlMatch[0].trim(); 39 | return { 40 | file: subtitleUrl, 41 | label: name, 42 | kind: "captions", 43 | }; 44 | }) 45 | .filter((subtitle: any) => subtitle.file !== null); 46 | 47 | return { 48 | server: this.name, 49 | source: { 50 | url: fileUrl, 51 | }, 52 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 53 | quality: getResolutionName(parseInt(quality, 10)), 54 | subtitles: subtitleArray, 55 | }; 56 | } catch (err) { 57 | if (err instanceof Error) this.logger.error(err.message); 58 | return undefined; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/fm22.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyFm22Extractor implements IExtractor { 8 | name = "Smashy (FM22)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/fm22.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data 23 | .match(/new\s+Playerjs\((\{[^]*?\})\);/)[1] 24 | .replace("id", '"id"') 25 | .replace("file", '"file"') 26 | .replace("subtitle", '"subtitle"') 27 | .replace(/,([^,]*)$/, "$1"), 28 | ); 29 | const fileUrl = config.file; 30 | 31 | const subtitleArray = config.subtitle 32 | .split(",") 33 | .map((entry: any) => { 34 | const nameRegex = /\[([^\]]*)\]/; 35 | const urlRegex = /https:\/\/s3\.bunnycdn\.ru\/[^\s,]+/g; 36 | const nameMatch = nameRegex.exec(entry); 37 | const urlMatch = urlRegex.exec(entry); 38 | const name = 39 | nameMatch && nameMatch[1].trim() 40 | ? nameMatch[1].trim() 41 | : urlMatch && urlMatch[1]; 42 | const subtitleUrl = urlMatch && urlMatch[0].trim(); 43 | return { 44 | file: subtitleUrl, 45 | label: name, 46 | kind: "captions", 47 | }; 48 | }) 49 | .filter((subtitle: any) => subtitle.file !== null); 50 | 51 | const quality = await getResolutionFromM3u8(fileUrl, true); 52 | return { 53 | server: this.name, 54 | source: { 55 | url: fileUrl, 56 | }, 57 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 58 | quality, 59 | subtitles: subtitleArray, 60 | }; 61 | } catch (err) { 62 | if (err instanceof Error) this.logger.error(err.message); 63 | return undefined; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/fx.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | 6 | export class SmashyFxExtractor implements IExtractor { 7 | name = "Smashy (Fx)"; 8 | 9 | logger = log.scope(this.name); 10 | 11 | url = "https://embed.smashystream.com/fx555.php"; 12 | 13 | referer = "https://remotestre.am/"; 14 | 15 | async extractUrl(url: string): Promise { 16 | try { 17 | const res = await axiosInstance.get(url, { 18 | headers: { 19 | referer: url, 20 | }, 21 | }); 22 | 23 | const file = res.data.match(/file:\s*"([^"]+)"/)[1]; 24 | 25 | return { 26 | server: this.name, 27 | source: { 28 | url: file, 29 | }, 30 | type: file.includes(".m3u8") ? "m3u8" : "mp4", 31 | quality: "Unknown", 32 | proxySettings: { 33 | type: file.includes(".m3u8") ? "m3u8" : "mp4", 34 | referer: this.referer, 35 | }, 36 | }; 37 | } catch (err) { 38 | if (err instanceof Error) this.logger.error(err.message); 39 | return undefined; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/im.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyImExtractor implements IExtractor { 8 | name = "Smashy (Im)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/im.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/new\s+Playerjs\((\{.*?\})\);/)[1], 23 | ); 24 | const fileUrl = config.file; 25 | 26 | const subtitleArray = config.subtitle 27 | .split(",") 28 | .map((entry: any) => { 29 | const nameRegex = /\[([^\]]*)\]/; 30 | const urlRegex = /https:\/\/cc\.2cdns\.com\/.*?\/(\w+-\d+)\.vtt/; 31 | const nameMatch = nameRegex.exec(entry); 32 | const urlMatch = urlRegex.exec(entry); 33 | const name = 34 | nameMatch && nameMatch[1].trim() 35 | ? nameMatch[1].trim() 36 | : urlMatch && urlMatch[1]; 37 | const subtitleUrl = urlMatch && urlMatch[0].trim(); 38 | return { 39 | file: subtitleUrl, 40 | label: name, 41 | kind: "captions", 42 | }; 43 | }) 44 | .filter((subtitle: any) => subtitle.file !== null); 45 | 46 | const quality = await getResolutionFromM3u8(fileUrl, true); 47 | return { 48 | server: this.name, 49 | source: { 50 | url: fileUrl, 51 | }, 52 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 53 | quality, 54 | subtitles: subtitleArray, 55 | }; 56 | } catch (err) { 57 | if (err instanceof Error) this.logger.error(err.message); 58 | return undefined; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/nflim.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionName } from "../utils"; 6 | 7 | export class SmashyNFlimExtractor implements IExtractor { 8 | name = "Smashy (NF)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/nflim.php"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const res = await axiosInstance.get(url, { 17 | headers: { 18 | referer: url, 19 | }, 20 | }); 21 | const config = JSON.parse( 22 | res.data.match(/var\s+config\s*=\s*({.*?});/)[1], 23 | ); 24 | const fileUrl = config.file.split(",")[0]; 25 | 26 | const vttArray = config.subtitle 27 | .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) 28 | .map((entry: any) => { 29 | const [, name, link] = entry.match( 30 | /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/, 31 | ); 32 | return { name, link: link.replace(",", "") }; 33 | }); 34 | 35 | const fileUrlRes = await axiosInstance.head(fileUrl.split("]")[1]); 36 | 37 | if (fileUrlRes.status !== 200) return undefined; 38 | 39 | return { 40 | server: this.name, 41 | source: { 42 | url: fileUrl.split("]")[1], 43 | }, 44 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 45 | quality: getResolutionName( 46 | parseInt(fileUrl.split("]")[0].split("[")[1], 10), 47 | ), 48 | subtitles: vttArray 49 | .filter((it: any) => !it.link.includes("thumbnails")) 50 | .map((subtitle: any) => ({ 51 | file: subtitle.link, 52 | label: subtitle.name, 53 | kind: "captions", 54 | })), 55 | thumbnails: { 56 | url: vttArray.find((it: any) => it.link.includes("thumbnails"))?.link, 57 | }, 58 | }; 59 | } catch (err) { 60 | if (err instanceof Error) this.logger.error(err.message); 61 | return undefined; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/segu.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | 6 | export class SmashySeguExtractor implements IExtractor { 7 | name = "Smashy (Se)"; 8 | 9 | logger = log.scope(this.name); 10 | 11 | url = "https://embed.smashystream.com/segu.php"; 12 | 13 | async extractUrl(url: string): Promise { 14 | try { 15 | const res = await axiosInstance.get(url, { 16 | headers: { 17 | referer: url, 18 | }, 19 | }); 20 | const config = JSON.parse( 21 | res.data.match(/new\s+Playerjs\((\{[^]*?\})\);/)[1].replace(/'/g, '"'), 22 | ); 23 | const fileUrl = config.file.split(",")[0].split("]")[1]; 24 | const quality = config.file.split(",")[0].split("]")[0].split("[")[1]; 25 | 26 | return { 27 | server: this.name, 28 | source: { 29 | url: fileUrl, 30 | }, 31 | type: fileUrl.includes(".m3u8") ? "m3u8" : "mp4", 32 | quality: quality.includes("K") ? quality : quality.toLowerCase(), 33 | }; 34 | } catch (err) { 35 | if (err instanceof Error) this.logger.error(err.message); 36 | return undefined; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/video1.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyVideo1Extractor implements IExtractor { 8 | name = "Smashy (V1)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/video1.php"; 13 | 14 | referer = "https://embed.smashystream.com/playere.php"; 15 | 16 | async extractUrl(url: string): Promise { 17 | try { 18 | const res = await axiosInstance.get(url, { 19 | headers: { 20 | referer: this.referer, 21 | }, 22 | }); 23 | 24 | const vttArray = res.data.subtitleUrls 25 | .match(/\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/g) 26 | .map((entry: any) => { 27 | const [, name, link] = entry.match( 28 | /\[([^\]]+)\](https?:\/\/\S+?)(?=,\[|$)/, 29 | ); 30 | return { name, link: link.replace(",", "") }; 31 | }); 32 | 33 | const quality = await getResolutionFromM3u8( 34 | res.data.sourceUrls[0], 35 | true, 36 | { 37 | referer: this.referer, 38 | }, 39 | ); 40 | 41 | return { 42 | server: this.name, 43 | source: { 44 | url: res.data.sourceUrls[0], 45 | }, 46 | type: "m3u8", 47 | quality, 48 | subtitles: vttArray 49 | .filter((it: any) => !it.link.includes("thumbnails")) 50 | .map((subtitle: any) => ({ 51 | file: subtitle.link, 52 | label: subtitle.name, 53 | kind: "captions", 54 | })), 55 | }; 56 | } catch (err) { 57 | if (err instanceof Error) this.logger.error(err.message); 58 | return undefined; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/video3m.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolutionFromM3u8 } from "../utils"; 6 | 7 | export class SmashyVideo3MExtractor implements IExtractor { 8 | name = "Smashy (3M)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/video1.php"; 13 | 14 | referer = "https://embed.smashystream.com/playere.php"; 15 | 16 | async extractUrl(url: string): Promise { 17 | try { 18 | const res = await axiosInstance.get(url, { 19 | headers: { 20 | referer: this.referer, 21 | }, 22 | }); 23 | const sourceUrl = res.data.sourceUrls.find( 24 | (s: any) => s.title === "English", 25 | ).file; 26 | if (!sourceUrl) throw new Error("No source url found"); 27 | 28 | const quality = await getResolutionFromM3u8(sourceUrl, true, { 29 | referer: this.referer, 30 | }); 31 | 32 | return { 33 | server: this.name, 34 | source: { 35 | url: sourceUrl, 36 | }, 37 | type: "m3u8", 38 | quality, 39 | }; 40 | } catch (err) { 41 | if (err instanceof Error) this.logger.error(err.message); 42 | return undefined; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/extractors/smashystream/watchx.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import crypto from "crypto"; 4 | import { axiosInstance } from "../../utils/axios"; 5 | import { IExtractor } from "../types"; 6 | 7 | export class SmashyWatchXExtractor implements IExtractor { 8 | name = "Smashy (WX)"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url = "https://embed.smashystream.com/watchx.php"; 13 | 14 | // Stolen from https://github.com/hexated/cloudstream-extensions-hexated/blob/cb11c787df613d58bf20e259d933530879670137/Ngefilm/src/main/kotlin/com/hexated/Extractors.kt#L53C30-L53C46 15 | // Tried to deobfuscate https://bestx.stream/assets/js/master_v5.js and https://bestx.stream/assets/js/crypto-js.js but i gave up 16 | private KEY = "4VqE3#N7zt&HEP^a"; 17 | 18 | async extractUrl(url: string): Promise { 19 | try { 20 | const res = await axiosInstance.get(url, { 21 | headers: { 22 | referer: url, 23 | }, 24 | }); 25 | 26 | const regex = /MasterJS\s*=\s*'([^']*)'/; 27 | const base64EncryptedData = regex.exec(res.data)![1]; 28 | const base64DecryptedData = JSON.parse( 29 | Buffer.from(base64EncryptedData, "base64").toString("utf8"), 30 | ); 31 | 32 | const derivedKey = crypto.pbkdf2Sync( 33 | this.KEY, 34 | Buffer.from(base64DecryptedData.salt, "hex"), 35 | base64DecryptedData.iterations, 36 | 32, 37 | "sha512", 38 | ); 39 | const decipher = crypto.createDecipheriv( 40 | "aes-256-cbc", 41 | derivedKey, 42 | Buffer.from(base64DecryptedData.iv, "hex"), 43 | ); 44 | decipher.setEncoding("utf8"); 45 | 46 | let decrypted = decipher.update( 47 | base64DecryptedData.ciphertext, 48 | "base64", 49 | "utf8", 50 | ); 51 | decrypted += decipher.final("utf8"); 52 | 53 | const sources = JSON.parse(decrypted.match(/sources: ([^\]]*\])/)![1]); 54 | const tracks = JSON.parse(decrypted.match(/tracks: ([^]*?\}\])/)![1]); 55 | 56 | const subtitles = tracks.filter((it: any) => it.kind === "captions"); 57 | const thumbnails = tracks.filter((it: any) => it.kind === "thumbnails"); 58 | 59 | return { 60 | server: this.name, 61 | source: { 62 | url: sources[0].file, 63 | }, 64 | type: sources[0].type === "hls" ? "m3u8" : "mp4", 65 | quality: sources[0].label, 66 | subtitles: subtitles.map((it: any) => ({ 67 | file: it.file, 68 | label: it.label, 69 | kind: it.kind, 70 | })), 71 | thumbnails: { 72 | url: thumbnails[0]?.file, 73 | }, 74 | }; 75 | } catch (err) { 76 | if (err instanceof Error) this.logger.error(err.message); 77 | return undefined; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/extractors/streamlare.ts: -------------------------------------------------------------------------------- 1 | import { app } from "electron"; 2 | import { Source } from "types/sources"; 3 | import log from "electron-log"; 4 | import { axiosInstance } from "../utils/axios"; 5 | import { IExtractor } from "./types"; 6 | 7 | export class StreamlareExtractor implements IExtractor { 8 | logger = log.scope("Streamlare"); 9 | 10 | url: string = "https://streamlare.com/"; 11 | 12 | referer: string = "https://sltube.org/"; 13 | 14 | async extractUrl(url: string): Promise { 15 | try { 16 | const id = url.split("/").pop(); 17 | // Streamlare endpoint requires the same userAgent that is used in the API request 18 | const userAgent = app.userAgentFallback; 19 | 20 | const res = await axiosInstance.post( 21 | `${this.url}api/video/stream/get`, 22 | { 23 | id, 24 | }, 25 | { 26 | headers: { 27 | "User-Agent": userAgent, 28 | }, 29 | }, 30 | ); 31 | 32 | if (res.data.result?.Original?.file) { 33 | return { 34 | server: "Streamlare", 35 | source: { 36 | url: res.data.result.Original.file, 37 | }, 38 | type: res.data.type.includes("mp4") ? "mp4" : "m3u8", 39 | quality: "Unknown", 40 | }; 41 | } 42 | return undefined; 43 | } catch (error) { 44 | if (error instanceof Error) this.logger.error(error.message); 45 | return undefined; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/extractors/streamwish.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "types/sources"; 2 | import log from "electron-log"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { IExtractor } from "./types"; 5 | import { getResolutionFromM3u8 } from "./utils"; 6 | 7 | export class StreamWishExtractor implements IExtractor { 8 | name = "StreamWish"; 9 | 10 | logger = log.scope(this.name); 11 | 12 | url: string = "https://streamwish.com"; 13 | 14 | async extractUrl( 15 | url: string, 16 | serverName?: string, 17 | ): Promise { 18 | try { 19 | const res = await axiosInstance.get(url); 20 | const regex = /sources: \[\s*{[^}]*file:\s*"([^"]+)"/; 21 | const file = regex.exec(res.data)?.[1]; 22 | if (!file) throw new Error("No file found"); 23 | return { 24 | server: serverName ?? this.name, 25 | source: { 26 | url: file, 27 | }, 28 | quality: await getResolutionFromM3u8(file, true), 29 | type: file.includes(".m3u8") ? "m3u8" : "mp4", 30 | }; 31 | } catch (error) { 32 | if (error instanceof Error) this.logger.error(error.message); 33 | return undefined; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/extractors/superstream/types.ts: -------------------------------------------------------------------------------- 1 | export interface TrailerUrlArr { 2 | url: string; 3 | img: string; 4 | } 5 | 6 | export interface SearchDataResponse { 7 | id: number; 8 | box_type: number; 9 | title: string; 10 | actors: string; 11 | description: string; 12 | poster_min: string; 13 | poster_org: string; 14 | poster: string; 15 | banner_mini: string; 16 | cats: string; 17 | content_rating: number; 18 | runtime: number; 19 | year: number; 20 | collect: number; 21 | view: number; 22 | download: number; 23 | imdb_rating: string; 24 | is_collect: number; 25 | "3d": number; 26 | audio_lang: string; 27 | quality_tag: string; 28 | lang: string; 29 | trailer_url_arr: TrailerUrlArr[]; 30 | trailer_url: string; 31 | } 32 | 33 | export interface SearchResponse { 34 | code: number; 35 | msg: string; 36 | data: { 37 | list: SearchDataResponse[]; 38 | }; 39 | } 40 | 41 | export interface List { 42 | path: string; 43 | quality: string; 44 | real_quality: string; 45 | format: string; 46 | size: string; 47 | size_bytes: unknown; 48 | count: number; 49 | dateline: number; 50 | fid: number; 51 | mmfid: number; 52 | h265: number; 53 | hdr: number; 54 | filename: string; 55 | original: number; 56 | colorbit: number; 57 | success: number; 58 | timeout: number; 59 | vip_link: number; 60 | fps: number; 61 | bitstream: string; 62 | width: number; 63 | height: number; 64 | } 65 | 66 | export interface Names { 67 | de: string; 68 | en: string; 69 | es: string; 70 | fr: string; 71 | ja: string; 72 | "pt-BR": string; 73 | ru: string; 74 | "zh-CN": string; 75 | } 76 | 77 | export interface Continent { 78 | code: string; 79 | geoname_id: number; 80 | names: Names; 81 | } 82 | 83 | export interface Names2 { 84 | de: string; 85 | en: string; 86 | es: string; 87 | fr: string; 88 | ja: string; 89 | "pt-BR": string; 90 | ru: string; 91 | "zh-CN": string; 92 | } 93 | 94 | export interface Country { 95 | geoname_id: number; 96 | is_in_european_union: boolean; 97 | iso_code: string; 98 | names: Names2; 99 | } 100 | 101 | export interface Names3 { 102 | de: string; 103 | en: string; 104 | es: string; 105 | fr: string; 106 | ja: string; 107 | "pt-BR": string; 108 | ru: string; 109 | "zh-CN": string; 110 | } 111 | 112 | export interface RegisteredCountry { 113 | geoname_id: number; 114 | is_in_european_union: boolean; 115 | iso_code: string; 116 | names: Names3; 117 | } 118 | 119 | export interface IpInfo { 120 | continent: Continent; 121 | country: Country; 122 | registered_country: RegisteredCountry; 123 | } 124 | 125 | export interface DownloadDataResponse { 126 | seconds: number; 127 | quality: string[]; 128 | list: List[]; 129 | ip: string; 130 | ip_info: IpInfo; 131 | } 132 | 133 | export interface DownloadResponse { 134 | code: number; 135 | msg: string; 136 | data: DownloadDataResponse; 137 | } 138 | 139 | export interface SuperStreamSubtitle { 140 | sid: number; 141 | mid: number; 142 | file_path: string; 143 | lang: string; 144 | language: string; 145 | delay: number; 146 | point: unknown; 147 | order: number; 148 | admin_order: number; 149 | myselect: number; 150 | add_time: number; 151 | count: number; 152 | } 153 | 154 | export interface SubtitleList { 155 | language: string; 156 | subtitles: SuperStreamSubtitle[]; 157 | } 158 | 159 | export interface SubtitleData { 160 | select: unknown[]; 161 | list: SubtitleList[]; 162 | } 163 | 164 | export interface SubtitleResponse { 165 | code: number; 166 | msg: string; 167 | data: SubtitleData; 168 | } 169 | -------------------------------------------------------------------------------- /src/main/extractors/types.ts: -------------------------------------------------------------------------------- 1 | import { LogFunctions } from "electron-log"; 2 | import { LiveMainPage, Source } from "types/sources"; 3 | import { ContentType } from "types/tmbd"; 4 | 5 | export interface IExtractor { 6 | name?: string; 7 | logger: LogFunctions; 8 | url: string; 9 | referer?: string; 10 | extractUrl?: ( 11 | url: string, 12 | serverName?: string, 13 | ) => Promise; 14 | extractUrls?: ( 15 | imdbId: string, 16 | type: ContentType, 17 | season?: number, 18 | episode?: number, 19 | ) => Promise; 20 | } 21 | 22 | export interface ILiveExtractor { 23 | name?: string; 24 | logger: LogFunctions; 25 | mainPageUrl: string; 26 | referer?: string; 27 | getMainPage: () => Promise; 28 | extractUrls: (url: string) => Promise; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/extractors/vegamovies/aiotechnical.ts: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "../../utils/axios"; 2 | 3 | const pen = (string: string) => { 4 | return string.replace(/[a-zA-Z]/g, (str) => { 5 | return String.fromCharCode( 6 | // @ts-ignore 7 | (str <= "Z" ? 0x5a : 0x7a) >= (str = str.charCodeAt(0x0) + 0xd) 8 | ? str 9 | : // @ts-ignore 10 | str - 0x1a, 11 | ); 12 | }); 13 | }; 14 | 15 | const getCookies = (data: string) => { 16 | const c = []; 17 | const ckRegExp = /ck\('([^']*)','([^']*)',([0-9.]+)\);/g; 18 | for (const match of data.matchAll(ckRegExp)) { 19 | const cookieName = match[1]; 20 | const cookieValue = match[2]; 21 | 22 | if (cookieName !== "_wp_http") { 23 | c.push(cookieValue); 24 | } 25 | } 26 | return c.join(""); 27 | }; 28 | 29 | export const extractAioTechnical = async (data: string): Promise => { 30 | const cookies = getCookies(data); 31 | const parsed = Buffer.from( 32 | Buffer.from(cookies, "base64").toString(), 33 | "base64", 34 | ).toString(); 35 | const decoded = pen(parsed); 36 | console.log(Buffer.from(decoded, "base64").toString()); 37 | const redirectData = JSON.parse(Buffer.from(decoded, "base64").toString()); 38 | const res = await axiosInstance.get( 39 | `${redirectData.blog_url}?re=${redirectData.data}`, 40 | { 41 | headers: { 42 | cookie: cookies, 43 | }, 44 | }, 45 | ); 46 | if (res.data.includes("Invalid Request")) 47 | throw new Error("Failed to get aiotechnical link, redirect error"); 48 | return res.request.res.responseUrl as string; 49 | }; 50 | -------------------------------------------------------------------------------- /src/main/extractors/vegamovies/gofile.ts: -------------------------------------------------------------------------------- 1 | import log from "electron-log"; 2 | import { Source } from "types/sources"; 3 | import { axiosInstance } from "../../utils/axios"; 4 | import { IExtractor } from "../types"; 5 | import { getResolution } from "../utils"; 6 | 7 | export class GoFileExtractor implements IExtractor { 8 | name = "GoFile"; 9 | 10 | url = "https://gofile.io"; 11 | 12 | apiUrl = "https://api.gofile.io"; 13 | 14 | logger = log.scope(this.name); 15 | 16 | private async getGoFileWebsiteToken() { 17 | const res = await axiosInstance.get(`${this.url}/dist/js/alljs.js`); 18 | const regex = /fetchData.websiteToken\s*=\s*"([^']+)"/; 19 | const websiteToken = res.data.match(regex)[1]; 20 | return websiteToken; 21 | } 22 | 23 | private async getGoFileAccountToken() { 24 | const res = await axiosInstance.get(`${this.apiUrl}/createAccount`); 25 | if (res.data.status === "ok") { 26 | return res.data.data.token as string; 27 | } 28 | } 29 | 30 | public async extractUrl(url: string): Promise { 31 | try { 32 | const linkData = await axiosInstance.get(url); 33 | const contentId = linkData.request.res.responseUrl.split("/d")[1]; 34 | const websiteToken = await this.getGoFileWebsiteToken(); 35 | const accountToken = await this.getGoFileAccountToken(); 36 | 37 | const goFileDownloadLink = await axiosInstance.get( 38 | `${this.apiUrl}/getContent?contentId=${contentId}&token=${websiteToken}&websiteToken=${accountToken}`, 39 | ); 40 | if (goFileDownloadLink.data.status === "ok") { 41 | return { 42 | server: "VegaMovies", 43 | source: { 44 | url: `${ 45 | goFileDownloadLink.data.data.contents[ 46 | goFileDownloadLink.data.data.childs[0] 47 | ].link 48 | }?accountToken=${accountToken}`, 49 | }, 50 | type: "mp4", 51 | proxySettings: { 52 | type: "mp4", 53 | }, 54 | quality: getResolution( 55 | goFileDownloadLink.data.data.contents[ 56 | goFileDownloadLink.data.data.childs[0] 57 | ].name, 58 | ), 59 | isVlc: true, 60 | }; 61 | } 62 | } catch (err) { 63 | if (err instanceof Error) this.logger.error(err.message); 64 | return undefined; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/extractors/vidplay.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "types/sources"; 2 | import log from "electron-log"; 3 | import { createCipheriv } from "crypto"; 4 | import { axiosInstance } from "../utils/axios"; 5 | import { IExtractor } from "./types"; 6 | import { getResolutionFromM3u8 } from "./utils"; 7 | 8 | export class VidPlayExtractor implements IExtractor { 9 | name = "VidPlay"; 10 | 11 | logger = log.scope(this.name); 12 | 13 | url = "https://vidplay.site"; 14 | 15 | referer = "https://vidplay.site/"; 16 | 17 | private async getKeys(): Promise { 18 | // Thanks to @Claudemirovsky for the keys :D 19 | const res = await axiosInstance.get( 20 | "https://raw.githubusercontent.com/Claudemirovsky/worstsource-keys/keys/keys.json", 21 | ); 22 | return res.data; 23 | } 24 | 25 | private async getEncodedId(sourceUrl: string): Promise { 26 | const id = sourceUrl.split("/e/")[1].split("?")[0]; 27 | const keyList = await this.getKeys(); 28 | const c1 = createCipheriv("rc4", Buffer.from(keyList[0]), ""); 29 | const c2 = createCipheriv("rc4", Buffer.from(keyList[1]), ""); 30 | 31 | let input = Buffer.from(id); 32 | input = Buffer.concat([c1.update(input), c1.final()]); 33 | input = Buffer.concat([c2.update(input), c2.final()]); 34 | 35 | return input.toString("base64").replace("/", "_"); 36 | } 37 | 38 | private async getFuTokenKey(sourceUrl: string) { 39 | const id = await this.getEncodedId(sourceUrl); 40 | const res = await axiosInstance.get(`${this.url}/futoken`, { 41 | headers: { 42 | referer: encodeURIComponent(sourceUrl), 43 | }, 44 | }); 45 | const fuKey = res.data.match(/var\s+k\s*=\s*'([^']+)'/)[1]; 46 | const a = []; 47 | for (let i = 0; i < id.length; i += 1) 48 | a.push(fuKey.charCodeAt(i % fuKey.length) + id.charCodeAt(i)); 49 | return `${fuKey},${a.join(",")}`; 50 | } 51 | 52 | private async getFileUrl(sourceUrl: string) { 53 | const futoken = await this.getFuTokenKey(sourceUrl); 54 | const url = `${this.url}/mediainfo/${futoken}?${sourceUrl.split("?")[1]}`; 55 | return url; 56 | } 57 | 58 | async extractUrl(url: string): Promise { 59 | try { 60 | const fileUrl = await this.getFileUrl(`${url}&autostart=true`); 61 | this.logger.debug(fileUrl); 62 | const res = await axiosInstance.get(fileUrl, { 63 | headers: { 64 | referer: url, 65 | }, 66 | }); 67 | const source = res.data.result.sources[0].file; 68 | 69 | const quality = await getResolutionFromM3u8(source, true); 70 | 71 | const thumbnail = res.data.result?.tracks?.find( 72 | (track: any) => track.kind === "thumbnails", 73 | ); 74 | return { 75 | server: this.name, 76 | source: { 77 | url: source, 78 | }, 79 | type: "m3u8", 80 | quality, 81 | thumbnails: { 82 | url: thumbnail?.file, 83 | }, 84 | }; 85 | } catch (error) { 86 | if (error instanceof Error) this.logger.error(error.message); 87 | return undefined; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/extractors/vidsrc.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { ContentType } from "types/tmbd"; 3 | import { Source, Subtitle } from "types/sources"; 4 | import log from "electron-log"; 5 | import fs from "fs"; 6 | import { axiosInstance } from "../utils/axios"; 7 | import { IExtractor } from "./types"; 8 | 9 | export class VidSrcExtractor implements IExtractor { 10 | logger = log.scope("VidSrc"); 11 | 12 | url = "https://vidsrc.me/"; 13 | 14 | referer = "https://vidsrc.stream/"; 15 | 16 | origin = "https://vidsrc.stream"; 17 | 18 | private embedUrl = `${this.url}embed/`; 19 | 20 | private subtitleUrl = "https://rest.opensubtitles.org/search/imdbid-"; 21 | 22 | private getHashBasedOnIndex(hash: string, index: string) { 23 | let result = ""; 24 | for (let i = 0; i < hash.length; i += 2) { 25 | const j = hash.substring(i, i + 2); 26 | result += String.fromCharCode( 27 | parseInt(j, 16) ^ index.charCodeAt((i / 2) % index.length), 28 | ); 29 | } 30 | return result; 31 | } 32 | 33 | async extractUrls( 34 | imdbId: string, 35 | type: ContentType, 36 | season?: number, 37 | episode?: number, 38 | ): Promise { 39 | try { 40 | const url = 41 | type === "movie" 42 | ? `${this.embedUrl}movie?imdb=${imdbId}` 43 | : `${this.embedUrl}tv?imdb=${imdbId}&season=${season}&episode=${episode}`; 44 | const res = await axiosInstance.get(url); 45 | const $ = load(res.data); 46 | 47 | const activeSourceUrl = `https:${$("#player_iframe").attr("src")}`; 48 | const srcRcpRes = await axiosInstance.get(activeSourceUrl, { 49 | headers: { 50 | referer: url, 51 | }, 52 | }); 53 | const srcRcpRes$ = load(srcRcpRes.data); 54 | const id = srcRcpRes$("body").attr("data-i"); 55 | const hash = srcRcpRes$("#hidden").attr("data-h"); 56 | if (!id || !hash) throw new Error("No id or hash found"); 57 | const sourceUrl = this.getHashBasedOnIndex(hash, id); 58 | const script = await axiosInstance.get(`https:${sourceUrl}`, { 59 | headers: { 60 | referer: url, 61 | }, 62 | }); 63 | const match = script.data 64 | .match(/file:"(.*?)"/)[1] 65 | .replace(/(\/\/\S+?=)/g, "") 66 | .replace("#2", ""); 67 | const finalUrl = Buffer.from(match, "base64").toString(); 68 | this.logger.debug(finalUrl); 69 | 70 | if (!finalUrl.includes("list.m3u8")) 71 | throw new Error("Something went wrong during url decoding"); 72 | 73 | const subtitleData = await axiosInstance.get( 74 | `${this.subtitleUrl}${imdbId}`, 75 | { 76 | headers: { 77 | "X-User-Agent": "trailers.to-UA", 78 | }, 79 | }, 80 | ); 81 | 82 | const reducedSubtitles = subtitleData.data.reduce( 83 | (accumulator: any, subtitle: any) => { 84 | const languageName = subtitle.LanguageName; 85 | accumulator[languageName] = accumulator[languageName] || []; 86 | if (accumulator[languageName].length < 5) { 87 | accumulator[languageName].push({ 88 | file: subtitle.SubDownloadLink, 89 | label: subtitle.LanguageName, 90 | kind: "captions", 91 | }); 92 | } 93 | return accumulator; 94 | }, 95 | {}, 96 | ); 97 | 98 | const finalSubtitles = Object.values( 99 | reducedSubtitles, 100 | ).flat() as Subtitle[]; 101 | 102 | return [ 103 | { 104 | server: "VidSrc Pro", 105 | source: { 106 | url: finalUrl, 107 | }, 108 | type: "m3u8", 109 | quality: "Unknown", 110 | subtitles: finalSubtitles, 111 | }, 112 | ]; 113 | } catch (error) { 114 | if (error instanceof Error) this.logger.error(error.message); 115 | return []; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/extractors/vidstream.ts: -------------------------------------------------------------------------------- 1 | import { Source } from "types/sources"; 2 | import log from "electron-log"; 3 | import { axiosInstance } from "../utils/axios"; 4 | import { IExtractor } from "./types"; 5 | import { getResolutionFromM3u8 } from "./utils"; 6 | 7 | export class VidstreamExtractor implements IExtractor { 8 | logger = log.scope("Vidstream"); 9 | 10 | url = "https://vidstream.pro/"; 11 | 12 | referer = "https://vidstream.pro/"; 13 | 14 | private eltikUrl = "https://9anime.eltik.net/"; 15 | 16 | private async getFuToken(referer: string) { 17 | const res = await axiosInstance.get(`${this.url}futoken`, { 18 | headers: { 19 | referer: encodeURIComponent(referer), 20 | }, 21 | }); 22 | const fuTokenWithoutComments = res.data.replace( 23 | /\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, 24 | "", 25 | ); 26 | return fuTokenWithoutComments; 27 | } 28 | 29 | private async getFileUrl(sourceUrl: string) { 30 | const futoken = await this.getFuToken(sourceUrl); 31 | const id = sourceUrl.split("e/")[1].split("?")[0]; 32 | 33 | const res = await axiosInstance.post( 34 | `${this.eltikUrl}rawVizcloud?query=${id}&apikey=lagrapps`, 35 | { 36 | query: id, 37 | futoken, 38 | }, 39 | ); 40 | return `${res.data.rawURL}?${sourceUrl.split("?")[1]}`; 41 | } 42 | 43 | async extractUrl(url: string): Promise { 44 | try { 45 | const fileUrl = await this.getFileUrl(`${url}&autostart=true`); 46 | const res = await axiosInstance.get(fileUrl, { 47 | headers: { 48 | referer: url, 49 | }, 50 | }); 51 | const source = res.data.result.sources[0].file; 52 | 53 | const quality = await getResolutionFromM3u8(source, true); 54 | 55 | const thumbnail = res.data.result?.tracks?.find( 56 | (track: any) => track.kind === "thumbnails", 57 | ); 58 | 59 | return { 60 | server: "Vidstream", 61 | source: { 62 | url: source, 63 | }, 64 | type: "m3u8", 65 | quality, 66 | thumbnails: { 67 | url: thumbnail?.file, 68 | }, 69 | }; 70 | } catch (error) { 71 | if (error instanceof Error) this.logger.error(error.message); 72 | return undefined; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/lib/m3u8-proxy.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ChildProcess, fork, spawn } from "child_process"; 3 | import log from "electron-log"; 4 | 5 | const logger = log.scope("M3U8 Proxy"); 6 | 7 | const PROXY_PATH = 8 | process.env.NODE_ENV === "production" 9 | ? path.join( 10 | process.resourcesPath, 11 | "node_modules/@warren-bank/hls-proxy/hls-proxy/bin/hlsd.js", 12 | ) 13 | : path.join( 14 | __dirname, 15 | "../../../node_modules/@warren-bank/hls-proxy/hls-proxy/bin/hlsd.js", 16 | ); 17 | 18 | let proxy: ChildProcess | null = null; 19 | 20 | interface StartM3U8ProxyOptions { 21 | referer?: string; 22 | origin?: string | null; 23 | userAgent?: string; 24 | } 25 | 26 | export const startM3U8Proxy = (options: StartM3U8ProxyOptions) => { 27 | try { 28 | logger.info("Starting M3U8 Proxy", PROXY_PATH); 29 | if (proxy) { 30 | proxy.kill(); 31 | } 32 | proxy = fork( 33 | path.join(PROXY_PATH), 34 | [ 35 | "--port", 36 | `7687`, 37 | "--host", 38 | `localhost`, 39 | `--referer "${options.referer}"`, 40 | "-v 4", 41 | `--origin "${options.origin}"`, 42 | `--user-agent "${options.userAgent}"`, 43 | ], 44 | { 45 | detached: true, 46 | env: { 47 | ELECTRON_RUN_AS_NODE: "1", 48 | }, 49 | }, 50 | ); 51 | proxy.on("exit", (code, signal) => { 52 | logger.info(`M3U8 Proxy exited with code ${code} and signal ${signal}`); 53 | }); 54 | } catch (error) { 55 | logger.error(JSON.stringify(error)); 56 | } 57 | }; 58 | 59 | export const stopM3U8Proxy = () => { 60 | if (proxy) { 61 | proxy.kill(); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/main/lib/mp4-proxy/command.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ChildProcess, fork } from "child_process"; 3 | import log from "electron-log"; 4 | 5 | const logger = log.scope("MP4 Proxy"); 6 | 7 | const PROXY_PATH = path.join(__dirname, "./proxy"); 8 | 9 | let proxy: ChildProcess | null = null; 10 | 11 | export const startProxy = () => { 12 | try { 13 | if (proxy) { 14 | proxy.kill(); 15 | } 16 | proxy = fork(path.join(PROXY_PATH), { 17 | detached: true, 18 | env: { 19 | ELECTRON_RUN_AS_NODE: "1", 20 | }, 21 | }); 22 | } catch (error) { 23 | logger.error(error); 24 | } 25 | }; 26 | 27 | export const stopProxy = () => { 28 | if (proxy) { 29 | proxy.kill(); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/main/lib/mp4-proxy/proxy.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const httpProxy = require("http-proxy"); 3 | 4 | const proxy = httpProxy.createProxyServer(); 5 | 6 | const server = express(); 7 | 8 | server.get("/proxy/:url", (req, res) => { 9 | const targetURL = new URL(decodeURIComponent(req.params.url)); 10 | const accountToken = req.query.accountToken; 11 | const referer = encodeURIComponent(req.query.referer); 12 | 13 | req.url = targetURL.toString(); 14 | 15 | proxy.web(req, res, { 16 | target: targetURL.origin, 17 | changeOrigin: true, 18 | headers: { 19 | host: targetURL.host, 20 | ...(accountToken && { 21 | cookie: `accountToken=${accountToken}`, 22 | }), 23 | ...(referer && { 24 | referer, 25 | }), 26 | }, 27 | }); 28 | }); 29 | 30 | server.listen(7688, () => { 31 | console.log("Server is running on port 3000"); 32 | }); 33 | -------------------------------------------------------------------------------- /src/main/lib/vlc.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, spawn } from "child_process"; 2 | import fs from "fs"; 3 | 4 | const firstPath = "C:\\Program Files\\VideoLAN\\VLC\\vlc.exe"; 5 | const secondPath = "C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"; 6 | 7 | let vlcInstance: ChildProcess | null = null; 8 | 9 | export const launchVlc = (url: string) => { 10 | if (vlcInstance) { 11 | vlcInstance.kill(); 12 | } 13 | 14 | if (fs.existsSync(firstPath)) { 15 | vlcInstance = spawn( 16 | `"${firstPath}"`, 17 | [url, "--audio-language=eng", "--sub-language=eng"], 18 | { shell: true }, 19 | ); 20 | return; 21 | } 22 | if (fs.existsSync(secondPath)) { 23 | vlcInstance = spawn( 24 | `"${secondPath}"`, 25 | [url, "--audio-language=eng", "--sub-language=eng"], 26 | { shell: true }, 27 | ); 28 | return; 29 | } 30 | throw new Error("VLC not found"); 31 | }; 32 | 33 | export const stopVlc = () => { 34 | if (vlcInstance) { 35 | vlcInstance.kill(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"; 2 | import { exposeElectronTRPC } from "electron-trpc/main"; 3 | 4 | export type Channels = 5 | | "app-close" 6 | | "app-update-available" 7 | | "app-update-available-confirm" 8 | | "app-download-progress"; 9 | 10 | const electronHandler = { 11 | ipcRenderer: { 12 | on(channel: Channels, func: (...args: unknown[]) => void) { 13 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 14 | func(...args); 15 | ipcRenderer.on(channel, subscription); 16 | 17 | return () => { 18 | ipcRenderer.removeListener(channel, subscription); 19 | }; 20 | }, 21 | once(channel: Channels, func: (...args: unknown[]) => void) { 22 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 23 | }, 24 | }, 25 | }; 26 | 27 | contextBridge.exposeInMainWorld("electron", electronHandler); 28 | 29 | process.once("loaded", () => { 30 | exposeElectronTRPC(); 31 | }); 32 | 33 | export type ElectronHandler = typeof electronHandler; 34 | -------------------------------------------------------------------------------- /src/main/util.ts: -------------------------------------------------------------------------------- 1 | import { URL } from "url"; 2 | import path from "path"; 3 | 4 | export function resolveHtmlPath(htmlFileName: string) { 5 | if (process.env.NODE_ENV === "development") { 6 | const port = process.env.PORT || 1212; 7 | const url = new URL(`http://localhost:${port}`); 8 | url.pathname = htmlFileName; 9 | return url.href; 10 | } 11 | return `file://${path.resolve(__dirname, "../renderer/", htmlFileName)}`; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const axiosInstance = axios.create({ 4 | timeout: 10000, 5 | }); 6 | -------------------------------------------------------------------------------- /src/main/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | 3 | export var CryptoJSAesJson = { 4 | encrypt: function (value: string, password: string) { 5 | return CryptoJS.AES.encrypt(JSON.stringify(value), password, { 6 | format: CryptoJSAesJson, 7 | }).toString(); 8 | }, 9 | decrypt: function (jsonStr: string, password: string) { 10 | return JSON.parse( 11 | CryptoJS.AES.decrypt(jsonStr, password, { 12 | format: CryptoJSAesJson, 13 | }).toString(CryptoJS.enc.Utf8), 14 | ); 15 | }, 16 | stringify: function (cipherParams: any) { 17 | var j: any = { 18 | ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64), 19 | }; 20 | if (cipherParams.iv) j.iv = cipherParams.iv.toString(); 21 | if (cipherParams.salt) j.s = cipherParams.salt.toString(); 22 | return JSON.stringify(j).replace(/\s/g, ""); 23 | }, 24 | parse: function (jsonStr: string) { 25 | var j = JSON.parse(jsonStr); 26 | var cipherParams = CryptoJS.lib.CipherParams.create({ 27 | ciphertext: CryptoJS.enc.Base64.parse(j.ct), 28 | }); 29 | if (j.iv) cipherParams.iv = CryptoJS.enc.Hex.parse(j.iv); 30 | if (j.s) cipherParams.salt = CryptoJS.enc.Hex.parse(j.s); 31 | return cipherParams; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/main/utils/vmContext.ts: -------------------------------------------------------------------------------- 1 | import vm, { Context } from "vm"; 2 | 3 | export const runInContext = (code: string, context: Context) => { 4 | vm.runInNewContext(code, context); 5 | return context; 6 | }; 7 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 2 | import { isAxiosError } from "axios"; 3 | import { ipcLink } from "electron-trpc/renderer"; 4 | import { useState } from "react"; 5 | import { MemoryRouter as Router, Routes, Route } from "react-router-dom"; 6 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 7 | import { Header } from "./components/layout/header"; 8 | import { UpdateModal } from "./components/update-modal"; 9 | import { Index } from "./pages"; 10 | import { ShowDetailsPage } from "./pages/shows/view"; 11 | import { SearchPage } from "./pages/shows/search"; 12 | import { SettingsPage } from "./pages/settings"; 13 | import { client } from "./api/trpc"; 14 | 15 | import LiveListPage from "./pages/live/list"; 16 | import LiveViewPage from "./pages/live/view"; 17 | import { ThemeProvider } from "./components/contexts/theme-provider"; 18 | import { toast } from "./components/ui/use-toast"; 19 | import { Toaster } from "./components/ui/toaster"; 20 | 21 | import "./styles/globals.css"; 22 | import { DiscoverPage } from "./pages/shows/discover"; 23 | 24 | const twentyFourHoursInMs = 1000 * 60 * 60 * 24; 25 | 26 | const App = () => { 27 | const [queryClient] = useState( 28 | () => 29 | new QueryClient({ 30 | defaultOptions: { 31 | queries: { 32 | retry: false, 33 | staleTime: twentyFourHoursInMs, 34 | 35 | onError: (error) => { 36 | if (isAxiosError(error)) { 37 | let message = ""; 38 | if (error.request.status === 401) message = "Unauthorized"; 39 | if (error.request.status === 404) message = "Not found"; 40 | if (error.request.status === 500) 41 | message = "Internal server error"; 42 | if (error.response?.data.status_message) 43 | message = error.response.data.status_message; 44 | 45 | toast({ 46 | title: "An error occurred, please try again later", 47 | description: message, 48 | variant: "destructive", 49 | duration: 5000, 50 | }); 51 | } 52 | if (error instanceof Error) { 53 | toast({ 54 | title: "An error occurred, please try again later", 55 | description: error.message, 56 | variant: "destructive", 57 | duration: 5000, 58 | }); 59 | } 60 | }, 61 | }, 62 | }, 63 | }), 64 | ); 65 | const [trpcClient] = useState(() => 66 | client.createClient({ 67 | links: [ipcLink()], 68 | }), 69 | ); 70 | return ( 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | } /> 83 | } /> 84 | } /> 85 | } 88 | /> 89 | } 92 | /> 93 | } /> 94 | } /> 95 | } /> 96 | 97 |
98 | 99 |
100 |
101 |
102 |
103 | ); 104 | }; 105 | 106 | export default App; 107 | -------------------------------------------------------------------------------- /src/renderer/api/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import { AppRouter } from "main/api"; 3 | 4 | export const client = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /src/renderer/components/contexts/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | export const COLOR_THEME = [ 4 | "zinc", 5 | "slate", 6 | "stone", 7 | "gray", 8 | "neutral", 9 | "red", 10 | "rose", 11 | "orange", 12 | "green", 13 | "blue", 14 | "yellow", 15 | "violet", 16 | ] as const; 17 | 18 | export type ColorTheme = (typeof COLOR_THEME)[number]; 19 | 20 | type Theme = "dark" | "light" | "system"; 21 | 22 | type ThemeProviderProps = { 23 | children: React.ReactNode; 24 | defaultTheme?: Theme; 25 | defaultColorTheme?: ColorTheme; 26 | storageKey?: string; 27 | }; 28 | 29 | type ThemeProviderState = { 30 | theme: Theme; 31 | setTheme: (theme: Theme) => void; 32 | colorTheme: ColorTheme; 33 | setColorTheme: (theme: ColorTheme) => void; 34 | }; 35 | 36 | const initialState: ThemeProviderState = { 37 | theme: "system", 38 | setTheme: () => null, 39 | colorTheme: "violet", 40 | setColorTheme: () => null, 41 | }; 42 | 43 | const ThemeProviderContext = createContext(initialState); 44 | 45 | export const ThemeProvider = ({ 46 | children, 47 | defaultTheme = "system", 48 | defaultColorTheme = "violet", 49 | storageKey = "vite-ui-theme", 50 | ...props 51 | }: ThemeProviderProps) => { 52 | const [theme, setTheme] = useState( 53 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, 54 | ); 55 | const [colorTheme, setColorTheme] = useState(defaultColorTheme); 56 | 57 | useEffect(() => { 58 | const root = window.document.documentElement; 59 | 60 | root.classList.remove("light", "dark"); 61 | 62 | if (theme === "system") { 63 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 64 | .matches 65 | ? "dark" 66 | : "light"; 67 | 68 | root.classList.add(systemTheme); 69 | return; 70 | } 71 | 72 | root.classList.add(theme); 73 | root.setAttribute("data-color-theme", colorTheme); 74 | }, [theme, colorTheme]); 75 | 76 | const value = { 77 | theme, 78 | setTheme: (theme: Theme) => { 79 | localStorage.setItem(storageKey, theme); 80 | setTheme(theme); 81 | }, 82 | colorTheme, 83 | setColorTheme: (theme: ColorTheme) => { 84 | setColorTheme(theme); 85 | }, 86 | }; 87 | 88 | return ( 89 | 90 | {children} 91 | 92 | ); 93 | }; 94 | 95 | export const useTheme = () => { 96 | const context = useContext(ThemeProviderContext); 97 | 98 | if (context === undefined) 99 | throw new Error("useTheme must be used within a ThemeProvider"); 100 | 101 | return context; 102 | }; 103 | -------------------------------------------------------------------------------- /src/renderer/components/layout/color-theme.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from "lucide-react"; 2 | import { cn } from "renderer/lib/utils"; 3 | import { COLOR_THEME, useTheme } from "../contexts/theme-provider"; 4 | import { Button } from "../ui/button"; 5 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; 6 | 7 | export const ColorThemePicker = () => { 8 | const { colorTheme, setColorTheme } = useTheme(); 9 | 10 | return ( 11 | 12 | 13 | Color Theme 14 | 15 | 16 |
17 | {COLOR_THEME.map((theme) => { 18 | const isActive = colorTheme === theme; 19 | 20 | return ( 21 | 54 | ); 55 | })} 56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/renderer/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import { SearchIcon, SettingsIcon } from "lucide-react"; 2 | import React, { FC, useState } from "react"; 3 | import { NavLink, useNavigate } from "react-router-dom"; 4 | import { client } from "renderer/api/trpc"; 5 | import { Button } from "../ui/button"; 6 | import { Input } from "../ui/input"; 7 | import { Spacer } from "../ui/spacer"; 8 | import { ThemeToggle } from "./theme-toggle"; 9 | 10 | const HEADER_LINKS = [ 11 | { 12 | title: "Live", 13 | url: "/live/list", 14 | }, 15 | { 16 | title: "Movies", 17 | url: "/shows/discover/movie", 18 | }, 19 | { 20 | title: "TV Shows", 21 | url: "/shows/discover/tv", 22 | }, 23 | ]; 24 | 25 | const Header: FC = () => { 26 | const [searchInput, setSearchInput] = useState(""); 27 | 28 | const { data } = client.app.getAppVersion.useQuery(); 29 | 30 | const navigate = useNavigate(); 31 | 32 | const handleSubmit = (e: React.FormEvent) => { 33 | e.preventDefault(); 34 | navigate(`/search/${searchInput}`); 35 | }; 36 | 37 | return ( 38 |
39 |
40 |
41 | 44 |
45 |
46 | {HEADER_LINKS.map((link) => ( 47 | 51 | [ 52 | isActive ? "border-b-2 border-primary" : "border-transparent", 53 | ].join(" ") 54 | } 55 | > 56 | 57 | 58 | ))} 59 | 60 |
61 |
62 | setSearchInput(e.target.value)} 67 | /> 68 | 75 |
76 |
77 | 82 | 83 |
84 |
85 |
86 | ); 87 | }; 88 | 89 | export { Header }; 90 | -------------------------------------------------------------------------------- /src/renderer/components/layout/loaders/skeleton-grid.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "../../ui/skeleton"; 2 | 3 | const SkeletonGrid = () => { 4 | return ( 5 |
6 | {Array.from({ length: 12 }, (_, i) => ( 7 | 8 | ))} 9 |
10 | ); 11 | }; 12 | 13 | export { SkeletonGrid }; 14 | -------------------------------------------------------------------------------- /src/renderer/components/layout/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { useTheme } from "../contexts/theme-provider"; 3 | import { Button } from "../ui/button"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from "../ui/dropdown-menu"; 10 | 11 | export const ThemeToggle = () => { 12 | const { setTheme } = useTheme(); 13 | 14 | return ( 15 | 16 | 17 | 22 | 23 | 24 | setTheme("light")}> 25 | Light 26 | 27 | setTheme("dark")}> 28 | Dark 29 | 30 | setTheme("system")}> 31 | System 32 | 33 | 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/renderer/components/pagination.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeft, ArrowRight, ChevronLeft, ChevronRight } from "lucide-react"; 2 | import React, { FC } from "react"; 3 | import { Button } from "./ui/button"; 4 | 5 | interface PaginationProps { 6 | currentPage: number; 7 | totalPages: number; 8 | onPageChange: (page: number) => void; 9 | } 10 | 11 | const Pagination: FC = ({ 12 | currentPage, 13 | totalPages, 14 | onPageChange, 15 | }) => { 16 | const handlePageChange = (page: number) => { 17 | if (page < 1 || page > totalPages) return; 18 | onPageChange(page); 19 | window.scrollTo({ 20 | top: 0, 21 | behavior: "smooth", 22 | }); 23 | }; 24 | 25 | const renderPageButtons = () => { 26 | const pageButtons = []; 27 | 28 | const createPageButton = (page: number, isActive: boolean) => ( 29 | 37 | ); 38 | 39 | const createEllipsis = (key: string) => ( 40 |

41 | ... 42 |

43 | ); 44 | 45 | const firstVisiblePage = Math.max(1, currentPage - 2); 46 | const lastVisiblePage = Math.min(totalPages, currentPage + 2); 47 | 48 | if (firstVisiblePage > 1) { 49 | pageButtons.push(createPageButton(1, false)); 50 | if (firstVisiblePage > 2) { 51 | pageButtons.push(createEllipsis("ellipsis1")); 52 | } 53 | } 54 | 55 | for (let i = firstVisiblePage; i <= lastVisiblePage; i += 1) { 56 | pageButtons.push(createPageButton(i, i === currentPage)); 57 | } 58 | 59 | if (lastVisiblePage < totalPages) { 60 | if (lastVisiblePage < totalPages - 1) { 61 | pageButtons.push(createEllipsis("ellipsis2")); 62 | } 63 | pageButtons.push(createPageButton(totalPages, false)); 64 | } 65 | 66 | return pageButtons; 67 | }; 68 | 69 | return ( 70 |
71 | 80 | 89 | {renderPageButtons()} 90 | 99 | 108 |
109 | ); 110 | }; 111 | 112 | export { Pagination }; 113 | -------------------------------------------------------------------------------- /src/renderer/components/player/source-selector.tsx: -------------------------------------------------------------------------------- 1 | import { Check, Star } from "lucide-react"; 2 | import React, { FC } from "react"; 3 | import { Source } from "types/sources"; 4 | import { Badge } from "../ui/badge"; 5 | import { Button } from "../ui/button"; 6 | 7 | interface SourceSelectorProps { 8 | sources: Source[]; 9 | activeSource: Source; 10 | selectSource: (source: Source) => void; 11 | } 12 | 13 | const SourceButton: FC< 14 | Omit & { source: Source } 15 | > = ({ source, activeSource, selectSource }) => { 16 | return ( 17 | 32 | ); 33 | }; 34 | 35 | const SourceSelector: FC = ({ 36 | sources, 37 | activeSource, 38 | selectSource, 39 | }) => { 40 | return ( 41 |
42 |
43 | {sources 44 | .filter((source) => !source.isVlc) 45 | .map((source) => ( 46 | 52 | ))} 53 |
54 | {sources.filter((source) => source.isVlc).length > 0 ? ( 55 |

Sources only playable through VLC

56 | ) : null} 57 |
58 | {sources 59 | .filter((source) => source.isVlc) 60 | .map((source) => ( 61 | 67 | ))} 68 |
69 |
70 | ); 71 | }; 72 | 73 | export { SourceSelector }; 74 | -------------------------------------------------------------------------------- /src/renderer/components/show-information.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { TMDB_IMAGE_BASE_URL } from "renderer/constants"; 3 | import { AspectRatio } from "./ui/aspect-ratio"; 4 | import { Badge } from "./ui/badge"; 5 | 6 | interface ShowInformationProps { 7 | posterPath: string; 8 | title: string; 9 | overview: string; 10 | releaseDate: string; 11 | runtime: number; 12 | genres: { name: string }[]; 13 | spokenLanguages: { name: string }[]; 14 | cast: { name: string; order: number }[]; 15 | productionCompanies: { name: string }[]; 16 | votingAverage: number; 17 | } 18 | 19 | const ShowInformation: FC = ({ 20 | posterPath, 21 | title, 22 | overview, 23 | releaseDate, 24 | runtime, 25 | genres, 26 | spokenLanguages, 27 | cast, 28 | productionCompanies, 29 | votingAverage, 30 | }) => { 31 | return ( 32 |
33 |
38 | 39 | 46 | 47 |
48 |
49 |

{title}

50 | 51 | TMDB: {votingAverage.toPrecision(2)} 52 | 53 |

{overview}

54 |
55 |

Release date: {releaseDate}

56 |

Duration: {runtime} min

57 |

Genres: {genres.map((g) => g.name).join(", ")}

58 |

Languages: {spokenLanguages.map((l) => l.name).join(", ")}

59 |

60 | Casts:{" "} 61 | {cast 62 | .sort((c) => c.order) 63 | .map((c) => c.name) 64 | .slice(0, 3) 65 | .join(", ")} 66 |

67 |

Production: {productionCompanies.map((c) => c.name).join(", ")}

68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export { ShowInformation }; 75 | -------------------------------------------------------------------------------- /src/renderer/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root; 4 | 5 | export { AspectRatio }; 6 | -------------------------------------------------------------------------------- /src/renderer/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import { cn } from "../../lib/utils"; 4 | 5 | const badgeVariants = cva( 6 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 7 | { 8 | variants: { 9 | variant: { 10 | default: 11 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 12 | secondary: 13 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 14 | destructive: 15 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 16 | outline: "text-foreground", 17 | orange: 18 | "border-transparent bg-orange-600 text-orange-100 hover:bg-orange-700", 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: "default", 23 | }, 24 | }, 25 | ); 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | const Badge = ({ className, variant, ...props }: BadgeProps) => { 32 | return ( 33 |
34 | ); 35 | }; 36 | 37 | export { Badge, badgeVariants }; 38 | -------------------------------------------------------------------------------- /src/renderer/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /src/renderer/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /src/renderer/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import { X } from "lucide-react"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const Dialog = DialogPrimitive.Root; 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger; 10 | 11 | const DialogPortal = DialogPrimitive.Portal; 12 | 13 | const DialogClose = DialogPrimitive.Close; 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )); 52 | DialogContent.displayName = DialogPrimitive.Content.displayName; 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ); 66 | DialogHeader.displayName = "DialogHeader"; 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ); 80 | DialogFooter.displayName = "DialogFooter"; 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )); 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )); 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogClose, 114 | DialogTrigger, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | }; 121 | -------------------------------------------------------------------------------- /src/renderer/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "../../lib/utils"; 4 | 5 | export type InputProps = React.InputHTMLAttributes; 6 | 7 | const Input = React.forwardRef( 8 | ({ className, type, ...props }, ref) => { 9 | return ( 10 | 19 | ); 20 | }, 21 | ); 22 | Input.displayName = "Input"; 23 | 24 | export { Input }; 25 | -------------------------------------------------------------------------------- /src/renderer/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "../../lib/utils"; 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /src/renderer/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 3 | import { cn } from "renderer/lib/utils"; 4 | 5 | const Popover = PopoverPrimitive.Root; 6 | 7 | const PopoverTrigger = PopoverPrimitive.Trigger; 8 | 9 | const PopoverContent = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 13 | ("div[data-media-player]") ?? 16 | document.body 17 | } 18 | > 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent }; 34 | -------------------------------------------------------------------------------- /src/renderer/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ProgressPrimitive from "@radix-ui/react-progress"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )); 24 | Progress.displayName = ProgressPrimitive.Root.displayName; 25 | 26 | export { Progress }; 27 | -------------------------------------------------------------------------------- /src/renderer/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )); 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )); 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 45 | 46 | export { ScrollArea, ScrollBar }; 47 | -------------------------------------------------------------------------------- /src/renderer/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /src/renderer/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "../../lib/utils"; 2 | 3 | const Skeleton = ({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) => { 7 | return ( 8 |
12 | ); 13 | }; 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/renderer/components/ui/spacer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | const Spacer: FC = () => { 4 | return
; 5 | }; 6 | 7 | export { Spacer }; 8 | -------------------------------------------------------------------------------- /src/renderer/components/ui/spinner-button.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | import { Button, ButtonProps } from "./button"; 3 | 4 | interface SpinnerButtonProps extends ButtonProps { 5 | isLoading: boolean; 6 | } 7 | 8 | export const SpinnerButton = ({ 9 | isLoading, 10 | children, 11 | ...props 12 | }: SpinnerButtonProps) => { 13 | return ( 14 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/renderer/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Toast, 3 | ToastClose, 4 | ToastDescription, 5 | ToastProvider, 6 | ToastTitle, 7 | ToastViewport, 8 | } from "./toast"; 9 | import { useToast } from "./use-toast"; 10 | 11 | export const Toaster = () => { 12 | const { toasts } = useToast(); 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ); 29 | })} 30 | 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/renderer/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 3 | 4 | import { cn } from "../../lib/utils"; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = TooltipPrimitive.Root; 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger; 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )); 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 27 | 28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 29 | -------------------------------------------------------------------------------- /src/renderer/components/update-modal.tsx: -------------------------------------------------------------------------------- 1 | import { UpdateInfo } from "electron-updater"; 2 | import { FC, useState } from "react"; 3 | import { client } from "renderer/api/trpc"; 4 | import { Button } from "./ui/button"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogFooter, 9 | DialogHeader, 10 | DialogTitle, 11 | } from "./ui/dialog"; 12 | 13 | const UpdateModal: FC = () => { 14 | const { mutate } = client.updater.quitAndInstall.useMutation(); 15 | const [isOpen, setIsOpen] = useState(false); 16 | const [releaseNotes, setReleaseNotes] = 17 | useState(null); 18 | 19 | const handleUpdate = () => { 20 | mutate(); 21 | }; 22 | 23 | const parseHTML = (htmlString: string) => { 24 | const parser = new DOMParser(); 25 | const doc = parser.parseFromString(htmlString, "text/html"); 26 | const ulElement = doc.querySelector("h3 + ul"); 27 | const liElements = ulElement ? Array.from(ulElement.children) : []; 28 | 29 | return liElements.map((liElement, index) => ( 30 |
  • {liElement.textContent}
  • 31 | )); 32 | }; 33 | 34 | window.electron.ipcRenderer.on("app-update-available", (info) => { 35 | setReleaseNotes((info as UpdateInfo).releaseNotes); 36 | setIsOpen(true); 37 | }); 38 | 39 | return ( 40 | 41 | 42 | 43 | Update available 44 | 45 |

    46 | There is a new update available. Do you want to restart and update? 47 |

    48 | {releaseNotes && typeof releaseNotes !== "string" && ( 49 |
      50 | {releaseNotes.map((release) => ( 51 |
    • 52 | v{release.version} 53 | {release.note && ( 54 |
        55 | {parseHTML(release.note)} 56 |
      57 | )} 58 |
    • 59 | ))} 60 |
    61 | )} 62 | 63 | 66 | 69 | 70 |
    71 |
    72 | ); 73 | }; 74 | 75 | export { UpdateModal }; 76 | -------------------------------------------------------------------------------- /src/renderer/constants.ts: -------------------------------------------------------------------------------- 1 | export const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p/w500"; 2 | -------------------------------------------------------------------------------- /src/renderer/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const OPENSUBTITLES_API_KEY: string; 2 | declare const TMDB_API_KEY: string; 3 | 4 | declare module "m3u8-parser"; 5 | -------------------------------------------------------------------------------- /src/renderer/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export const useLocalStorage = (key: string, initialValue: T) => { 4 | // State to store our value 5 | // Pass initial state function to useState so logic is only executed once 6 | const [storedValue, setStoredValue] = useState(() => { 7 | if (typeof window === "undefined") { 8 | return initialValue; 9 | } 10 | try { 11 | // Get from local storage by key 12 | const item = window.localStorage.getItem(key); 13 | // Parse stored json or if none return initialValue 14 | return item ? JSON.parse(item) : initialValue; 15 | } catch (error) { 16 | // If error also return initialValue 17 | console.log(error); 18 | return initialValue; 19 | } 20 | }); 21 | // Return a wrapped version of useState's setter function that ... 22 | // ... persists the new value to localStorage. 23 | const setValue = (value: T | ((val: T) => T)) => { 24 | try { 25 | // Allow value to be a function so we have same API as useState 26 | const valueToStore = 27 | value instanceof Function ? value(storedValue) : value; 28 | // Save state 29 | setStoredValue(valueToStore); 30 | // Save to local storage 31 | if (typeof window !== "undefined") { 32 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 33 | } 34 | } catch (error) { 35 | // A more advanced implementation would handle the error case 36 | console.log(error); 37 | } 38 | }; 39 | return [storedValue, setValue] as const; 40 | }; 41 | -------------------------------------------------------------------------------- /src/renderer/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { useLocation } from "react-router-dom"; 3 | 4 | export const useQuery = () => { 5 | const { search } = useLocation(); 6 | 7 | const query = useMemo(() => new URLSearchParams(search), [search]); 8 | 9 | const get = (param: string): T => { 10 | const value = query.get(param); 11 | if (value === null) { 12 | throw new Error(`Parameter "${param}" is missing.`); 13 | } 14 | return value as unknown as T; 15 | }; 16 | 17 | return { get }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/renderer/hooks/useRequiredParams.ts: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | 3 | export const useRequiredParams = >() => { 4 | const params = useParams(); 5 | return params as T; 6 | }; 7 | -------------------------------------------------------------------------------- /src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Restreamer 6 | 7 | 8 |
    9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | 4 | const container = document.getElementById("root")!; 5 | const root = createRoot(container); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /src/renderer/lib/proxy.ts: -------------------------------------------------------------------------------- 1 | const M3U8_PROXY_URL = "http://127.0.0.1:7687"; 2 | const MP4_PROXY_URL = "http://127.0.0.1:7688/proxy"; 3 | const FILE_EXTENSION = ".m3u8"; 4 | 5 | export const getM3U8ProxyUrl = (m3u8Link: string, referer?: string) => { 6 | const hlsProxyUrl = `${M3U8_PROXY_URL}/${Buffer.from( 7 | `${m3u8Link}|${referer}`, 8 | "binary", 9 | ).toString("base64")}${FILE_EXTENSION}`; 10 | 11 | return hlsProxyUrl; 12 | }; 13 | 14 | export const getMP4ProxyUrl = (mp4Link: string, referer?: string) => { 15 | let mp4ProxyUrl = `${MP4_PROXY_URL}/${encodeURIComponent(mp4Link)}`; 16 | if (referer) { 17 | mp4ProxyUrl += `?referer=${encodeURIComponent(referer)}`; 18 | } 19 | 20 | return mp4ProxyUrl; 21 | }; 22 | -------------------------------------------------------------------------------- /src/renderer/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/pages/live/view.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MediaPlayer, 3 | MediaPlayerInstance, 4 | MediaProvider, 5 | } from "@vidstack/react"; 6 | import { 7 | defaultLayoutIcons, 8 | DefaultVideoLayout, 9 | } from "@vidstack/react/player/layouts/default"; 10 | import React, { FC, useEffect, useRef, useState } from "react"; 11 | import { useNavigate } from "react-router-dom"; 12 | import { client } from "renderer/api/trpc"; 13 | import { SourceSelector } from "renderer/components/player/source-selector"; 14 | import { useToast } from "renderer/components/ui/use-toast"; 15 | import { useQuery } from "renderer/hooks/useQuery"; 16 | import { getM3U8ProxyUrl } from "renderer/lib/proxy"; 17 | import { Source } from "types/sources"; 18 | import { useLocalStorage } from "usehooks-ts"; 19 | 20 | const LiveViewPage: FC = () => { 21 | const { toast } = useToast(); 22 | const navigate = useNavigate(); 23 | const player = useRef(null); 24 | const query = useQuery(); 25 | const url = decodeURIComponent(query.get("url")); 26 | 27 | const [playerVolume, setPlayerVolume] = useLocalStorage( 28 | "playerVolume", 29 | 1, 30 | ); 31 | 32 | const { data, isLoading } = client.live.getLiveUrl.useQuery( 33 | { url }, 34 | { 35 | enabled: !!url, 36 | }, 37 | ); 38 | 39 | const { mutate: startProxy } = client.proxy.start.useMutation(); 40 | const { mutate: stopProxy } = client.proxy.stop.useMutation(); 41 | 42 | const [sources, setSources] = useState(data ?? []); 43 | const [selectedSource, setSelectedSource] = useState(null); 44 | 45 | useEffect(() => { 46 | if (data && (!sources || sources.length === 0)) setSources(data); 47 | 48 | if (sources && sources.length > 0 && !selectedSource) { 49 | setSelectedSource(sources[0]); 50 | } 51 | }, [data, sources, selectedSource]); 52 | 53 | useEffect(() => { 54 | if (!isLoading && data && data.length === 0) { 55 | toast({ 56 | title: "No sources found", 57 | description: "No sources found for this video", 58 | variant: "destructive", 59 | }); 60 | 61 | navigate(-1); 62 | } 63 | // eslint-disable-next-line react-hooks/exhaustive-deps 64 | }, [isLoading, data]); 65 | 66 | useEffect(() => { 67 | if (selectedSource && selectedSource.proxySettings) { 68 | startProxy(selectedSource.proxySettings); 69 | } else { 70 | stopProxy(); 71 | } 72 | return () => { 73 | stopProxy(); 74 | }; 75 | }, [selectedSource, startProxy, stopProxy]); 76 | 77 | useEffect(() => { 78 | return () => { 79 | setSelectedSource(null); 80 | }; 81 | }, []); 82 | 83 | return ( 84 |
    85 | {selectedSource && ( 86 | { 103 | // if (!e.volume) return; 104 | // setPlayerVolume(e.volume); 105 | // }} 106 | > 107 | 108 | 109 | 110 | )} 111 | {selectedSource && ( 112 | 117 | )} 118 |
    119 | ); 120 | }; 121 | 122 | export default LiveViewPage; 123 | -------------------------------------------------------------------------------- /src/renderer/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { ColorThemePicker } from "renderer/components/layout/color-theme"; 3 | import { OpenSubtitlesSettings } from "renderer/components/settings/opensubtitles"; 4 | import { SourcesCheck } from "renderer/components/settings/sources-check"; 5 | 6 | const SettingsPage: FC = () => { 7 | return ( 8 |
    9 | 10 | 11 | 12 |
    13 | ); 14 | }; 15 | 16 | export { SettingsPage }; 17 | -------------------------------------------------------------------------------- /src/renderer/pages/shows/discover.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | import ShowFilter, { FilterOptions } from "renderer/components/show-filters"; 3 | import { Link } from "react-router-dom"; 4 | import { SkeletonGrid } from "renderer/components/layout/loaders/skeleton-grid"; 5 | import { Pagination } from "renderer/components/pagination"; 6 | import { client } from "renderer/api/trpc"; 7 | import { TMDB_IMAGE_BASE_URL } from "renderer/constants"; 8 | import { Separator } from "renderer/components/ui/separator"; 9 | import { 10 | Tooltip, 11 | TooltipContent, 12 | TooltipProvider, 13 | TooltipTrigger, 14 | } from "renderer/components/ui/tooltip"; 15 | import { AspectRatio } from "renderer/components/ui/aspect-ratio"; 16 | import { Badge } from "renderer/components/ui/badge"; 17 | import { useRequiredParams } from "renderer/hooks/useRequiredParams"; 18 | import { ContentType } from "types/tmbd"; 19 | 20 | const DiscoverPage: FC = () => { 21 | const params = useRequiredParams<{ 22 | mediaType: ContentType; 23 | }>(); 24 | 25 | const [options, setOptions] = useState({ 26 | genres: [], 27 | sortBy: "popularity.desc", 28 | year: undefined, 29 | type: params.mediaType, 30 | page: 1, 31 | }); 32 | 33 | const { data, isLoading } = client.tmdb.discover.useQuery({ 34 | options: { 35 | genres: options.genres, 36 | sortBy: options.sortBy, 37 | year: options.year, 38 | page: options.page, 39 | }, 40 | type: options.type, 41 | }); 42 | 43 | const callbackHandler = (opts: FilterOptions) => { 44 | setOptions(opts); 45 | }; 46 | 47 | const onPageChange = (page: number) => { 48 | setOptions({ ...options, page }); 49 | }; 50 | 51 | useEffect(() => { 52 | setOptions({ ...options, type: params.mediaType }); 53 | }, [params.mediaType]); 54 | 55 | return ( 56 |
    57 | 58 | 59 | {isLoading && } 60 | {data && ( 61 | <> 62 |
    63 | {data.results.map((show) => { 64 | return ( 65 | show.poster_path && ( 66 |
    67 | 68 | 69 | {show.name} 73 | 74 |
    75 | 76 | 77 | 78 |

    79 | {show.name ? show.name : show.title} 80 |

    81 |
    82 | 83 |

    {show.name ? show.name : show.title}

    84 |
    85 |
    86 |
    87 |
    88 |

    89 | {new Date( 90 | show.release_date 91 | ? show.release_date 92 | : show.first_air_date, 93 | ).getFullYear() || "N/A"} 94 |

    95 | 96 | {show.media_type === "tv" ? "TV" : "Movie"} 97 | 98 |
    99 |
    100 | 101 |
    102 | ) 103 | ); 104 | })} 105 |
    106 | 500 ? 500 : data.total_pages} 109 | onPageChange={onPageChange} 110 | /> 111 | 112 | )} 113 |
    114 | ); 115 | }; 116 | 117 | export { DiscoverPage }; 118 | -------------------------------------------------------------------------------- /src/renderer/pages/shows/search.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Link, useParams } from "react-router-dom"; 3 | import { SkeletonGrid } from "renderer/components/layout/loaders/skeleton-grid"; 4 | import { client } from "renderer/api/trpc"; 5 | import { TMDB_IMAGE_BASE_URL } from "renderer/constants"; 6 | import { AspectRatio } from "renderer/components/ui/aspect-ratio"; 7 | import { Badge } from "renderer/components/ui/badge"; 8 | import { 9 | Tooltip, 10 | TooltipContent, 11 | TooltipProvider, 12 | TooltipTrigger, 13 | } from "renderer/components/ui/tooltip"; 14 | 15 | const SearchPage: FC = () => { 16 | const { query } = useParams(); 17 | 18 | const { data, isLoading } = client.tmdb.search.useQuery( 19 | { 20 | query, 21 | }, 22 | { enabled: !!query }, 23 | ); 24 | 25 | if (isLoading) return ; 26 | 27 | return ( 28 |
    29 | {data?.results.map((show) => { 30 | return ( 31 | show.poster_path && ( 32 |
    33 | 34 | 35 | {show.name} 39 | 40 |
    41 | 42 | 43 | 44 |

    45 | {show.name ? show.name : show.title} 46 |

    47 |
    48 | 49 |

    {show.name ? show.name : show.title}

    50 |
    51 |
    52 |
    53 |
    54 |

    55 | {new Date( 56 | show.release_date 57 | ? show.release_date 58 | : show.first_air_date, 59 | ).getFullYear() || "N/A"} 60 |

    61 | {show.media_type === "tv" ? "TV" : "Movie"} 62 |
    63 |
    64 | 65 |
    66 | ) 67 | ); 68 | })} 69 |
    70 | ); 71 | }; 72 | 73 | export { SearchPage }; 74 | -------------------------------------------------------------------------------- /src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronHandler } from "main/preload"; 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronHandler; 6 | } 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /src/renderer/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const randomString = (length: number): string => { 2 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 3 | const charactersLength = characters.length; 4 | let result = ""; 5 | for (let i = 0; i < length; i += 1) { 6 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 7 | } 8 | return result; 9 | }; 10 | -------------------------------------------------------------------------------- /src/types/localstorage.ts: -------------------------------------------------------------------------------- 1 | export interface PlayingData { 2 | [key: string]: { 3 | season?: number; 4 | episode?: number; 5 | playingTime: number; 6 | duration: number; 7 | }; 8 | } 9 | 10 | export type LiveFavorites = string[]; 11 | -------------------------------------------------------------------------------- /src/types/sources.ts: -------------------------------------------------------------------------------- 1 | export interface Subtitle { 2 | file: string; 3 | label: string; 4 | kind: string; 5 | } 6 | 7 | export interface Source { 8 | source: { 9 | url: string; 10 | requiresBlob?: boolean; 11 | }; 12 | server: string; 13 | type: "m3u8" | "mp4" | "mkv"; 14 | quality: 15 | | "4K" 16 | | "1440p" 17 | | "1080p" 18 | | "808p" 19 | | "720p" 20 | | "480p" 21 | | "360p" 22 | | "240p" 23 | | "144p" 24 | | "720p/1080p" 25 | | "Unknown"; 26 | proxySettings?: { 27 | type: "mp4" | "m3u8"; 28 | referer?: string; 29 | origin?: string | null; 30 | userAgent?: string; 31 | }; 32 | subtitles?: Subtitle[]; 33 | thumbnails?: { 34 | url: string; 35 | requiresBlob?: boolean; 36 | }; 37 | isVlc?: boolean; 38 | labels?: { 39 | hasSubtitles?: boolean; 40 | }; 41 | } 42 | 43 | export interface LiveMainPage { 44 | imgSrc: string; 45 | title: string; 46 | url: string; 47 | } 48 | -------------------------------------------------------------------------------- /src/types/tmbd.ts: -------------------------------------------------------------------------------- 1 | export type ContentType = "movie" | "tv"; 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | zinc: "hsl(var(--primary-zinc))", 54 | slate: "hsl(var(--primary-slate))", 55 | stone: "hsl(var(--primary-stone))", 56 | gray: "hsl(var(--primary-gray))", 57 | neutral: "hsl(var(--primary-neutral))", 58 | red: "hsl(var(--primary-red))", 59 | rose: "hsl(var(--primary-rose))", 60 | orange: "hsl(var(--primary-orange))", 61 | green: "hsl(var(--primary-green))", 62 | blue: "hsl(var(--primary-blue))", 63 | yellow: "hsl(var(--primary-yellow))", 64 | violet: "hsl(var(--primary-violet))", 65 | }, 66 | borderRadius: { 67 | lg: "var(--radius)", 68 | md: "calc(var(--radius) - 2px)", 69 | sm: "calc(var(--radius) - 4px)", 70 | }, 71 | keyframes: { 72 | "accordion-down": { 73 | from: { height: 0 }, 74 | to: { height: "var(--radix-accordion-content-height)" }, 75 | }, 76 | "accordion-up": { 77 | from: { height: "var(--radix-accordion-content-height)" }, 78 | to: { height: 0 }, 79 | }, 80 | }, 81 | animation: { 82 | "accordion-down": "accordion-down 0.2s ease-out", 83 | "accordion-up": "accordion-up 0.2s ease-out", 84 | }, 85 | }, 86 | }, 87 | // eslint-disable-next-line global-require 88 | plugins: [require("tailwindcss-animate")], 89 | }; 90 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2021"], 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "noEmitOnError": true, 10 | "sourceMap": true, 11 | "baseUrl": "./src", 12 | "moduleResolution": "node", 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "resolveJsonModule": true, 16 | "allowJs": true, 17 | "outDir": ".erb/dll" 18 | }, 19 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 20 | } 21 | --------------------------------------------------------------------------------