├── public
├── favicon.ico
├── img
│ ├── 4_login.png
│ ├── 1_welcome.png
│ ├── 2_connect.png
│ └── 3_connected.png
├── placeholder.png
├── robots.txt
├── manifest.json
└── index.html
├── .prettierrc
├── config.example.json
├── src
├── setupTests.ts
├── index.tsx
├── reportWebVitals.ts
├── App.tsx
├── utils
│ ├── Base64Utils.test.ts
│ ├── RequestUtils.ts
│ └── Base64Utils.ts
├── hooks
│ └── useLNC.ts
├── react-app-env.d.ts
├── logo.svg
├── pages
│ ├── Connect.tsx
│ ├── Login.tsx
│ └── Home.tsx
├── zeus-logo.svg
└── components
│ └── Page.tsx
├── config
├── webpack
│ └── persistentCache
│ │ └── createEnvironmentHash.js
├── jest
│ ├── cssTransform.js
│ ├── babelTransform.js
│ └── fileTransform.js
├── getHttpsConfig.js
├── paths.js
├── modules.js
├── env.js
├── webpackDevServer.config.js
└── webpack.config.js
├── .gitignore
├── .prettierrc.json
├── README.md
├── tsconfig.json
├── scripts
├── test.js
├── start.js
└── build.js
├── package.json
└── LICENSE
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/4_login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/img/4_login.png
--------------------------------------------------------------------------------
/public/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/placeholder.png
--------------------------------------------------------------------------------
/public/img/1_welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/img/1_welcome.png
--------------------------------------------------------------------------------
/public/img/2_connect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/img/2_connect.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/img/3_connected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZeusLN/echo/HEAD/public/img/3_connected.png
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "tabWidth": 4,
4 | "semi": true,
5 | "trailingComma": "none",
6 | "bracketSpacing": true
7 | }
8 |
--------------------------------------------------------------------------------
/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "podcastIndexAPIKey": "XXXXXXXXXXXXXXXXXXXX",
3 | "podcastIndexSecretKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
4 | }
5 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/config/webpack/persistentCache/createEnvironmentHash.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const { createHash } = require('crypto');
3 |
4 | module.exports = env => {
5 | const hash = createHash('md5');
6 | hash.update(JSON.stringify(env));
7 |
8 | return hash.digest('hex');
9 | };
10 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 | build.zip
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # config
27 | config.json
28 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "overrides": [
3 | {
4 | "files": [".prettierrc", ".babelrc", ".eslintrc", ".stylelintrc"],
5 | "options": {
6 | "parser": "json"
7 | }
8 | }
9 | ],
10 | "printWidth": 90,
11 | "proseWrap": "always",
12 | "singleQuote": true,
13 | "useTabs": false,
14 | "semi": true,
15 | "tabWidth": 2,
16 | "trailingComma": "all",
17 | "bracketSpacing": true,
18 | "jsxBracketSameLine": false,
19 | "arrowParens": "avoid"
20 | }
21 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import 'bootstrap/dist/css/bootstrap.min.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render();
11 |
12 | // If you want to start measuring performance in your app, pass a function
13 | // to log results (for example: reportWebVitals(console.log))
14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
15 | reportWebVitals();
16 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(
6 | ({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
7 | getCLS(onPerfEntry);
8 | getFID(onPerfEntry);
9 | getFCP(onPerfEntry);
10 | getLCP(onPerfEntry);
11 | getTTFB(onPerfEntry);
12 | }
13 | );
14 | }
15 | };
16 |
17 | export default reportWebVitals;
18 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Echo
2 |
3 | A Podcasting 2.0 web player that connects to your own node using Lightning Node Connect
4 |
5 |
6 |
7 | ## Getting started
8 |
9 | 1. `npm i`
10 | 2. `cp config.example.json config.json`
11 | 3. Populate `config.json` with credentials from https://api.podcastindex.org/developer_home
12 | 4. `npm start`
13 |
14 | Lightning Node Connect documentation: https://docs.lightning.engineering/lightning-network-tools/lightning-terminal/lightning-node-connect
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter, Route, Routes } from 'react-router-dom';
3 | import Connect from './pages/Connect';
4 | import Home from './pages/Home';
5 | import Login from './pages/Login';
6 |
7 | function App() {
8 | return (
9 | <>
10 |
11 |
12 | } />
13 | } />
14 | } />
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | export default App;
22 |
--------------------------------------------------------------------------------
/config/jest/babelTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const babelJest = require('babel-jest').default;
4 |
5 | const hasJsxRuntime = (() => {
6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
7 | return false;
8 | }
9 |
10 | try {
11 | require.resolve('react/jsx-runtime');
12 | return true;
13 | } catch (e) {
14 | return false;
15 | }
16 | })();
17 |
18 | module.exports = babelJest.createTransformer({
19 | presets: [
20 | [
21 | require.resolve('babel-preset-react-app'),
22 | {
23 | runtime: hasJsxRuntime ? 'automatic' : 'classic',
24 | },
25 | ],
26 | ],
27 | babelrc: false,
28 | configFile: false,
29 | });
30 |
--------------------------------------------------------------------------------
/src/utils/Base64Utils.test.ts:
--------------------------------------------------------------------------------
1 | import { utf8ToHexString } from './Base64Utils';
2 |
3 | describe('Base64Utils', () => {
4 | describe('utf8ToHexString', () => {
5 | it('Converts utf8 string to hexidecimal string', () => {
6 | expect(utf8ToHexString('Test string with punctuation.')).toEqual(
7 | '5465737420737472696e6720776974682070756e6374756174696f6e2e'
8 | );
9 | expect(utf8ToHexString('1234567890')).toEqual(
10 | '31323334353637383930'
11 | );
12 | expect(utf8ToHexString('!@#$%^&*')).toEqual('21402324255e262a');
13 | expect(utf8ToHexString('áéíÓÚ àèìÒÙ âêîÔÛ')).toEqual(
14 | 'c3a1c3a9c3adc393c39a20c3a0c3a8c3acc392c39920c3a2c3aac3aec394c39b'
15 | );
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/hooks/useLNC.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import LNC from '@lightninglabs/lnc-web';
3 |
4 | // create a singleton instance of LNC that will live for the lifetime of the app
5 | const lnc = new LNC({});
6 |
7 | /**
8 | * A hook that exposes a single LNC instance of LNC to all component that need it.
9 | * It also returns a couple helper functions to simplify the usage of LNC
10 | */
11 | const useLNC = () => {
12 | /** Connects to LNC using the provided pairing phrase and password */
13 | const connect = useCallback(
14 | async (pairingPhrase: string, password: string) => {
15 | lnc.credentials.pairingPhrase = pairingPhrase;
16 | await lnc.connect();
17 | // verify we can fetch data
18 | await lnc.lnd.lightning.listChannels();
19 | // set the password after confirming the connection works
20 | lnc.credentials.password = password;
21 | },
22 | []
23 | );
24 |
25 | /** Connects to LNC using the password to decrypt the stored keys */
26 | const login = useCallback(async (password: string) => {
27 | lnc.credentials.password = password;
28 | await lnc.connect();
29 | }, []);
30 |
31 | return { lnc, connect, login };
32 | };
33 |
34 | export default useLNC;
35 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 | const jest = require('jest');
19 | const execSync = require('child_process').execSync;
20 | let argv = process.argv.slice(2);
21 |
22 | function isInGitRepository() {
23 | try {
24 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
25 | return true;
26 | } catch (e) {
27 | return false;
28 | }
29 | }
30 |
31 | function isInMercurialRepository() {
32 | try {
33 | execSync('hg --cwd . root', { stdio: 'ignore' });
34 | return true;
35 | } catch (e) {
36 | return false;
37 | }
38 | }
39 |
40 | // Watch unless on CI or explicitly running all tests
41 | if (
42 | !process.env.CI &&
43 | argv.indexOf('--watchAll') === -1 &&
44 | argv.indexOf('--watchAll=false') === -1
45 | ) {
46 | // https://github.com/facebook/create-react-app/issues/5210
47 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
48 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
49 | }
50 |
51 |
52 | jest.run(argv);
53 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | declare namespace NodeJS {
6 | interface ProcessEnv {
7 | readonly NODE_ENV: 'development' | 'production' | 'test';
8 | readonly PUBLIC_URL: string;
9 | }
10 | }
11 |
12 | declare module '*.avif' {
13 | const src: string;
14 | export default src;
15 | }
16 |
17 | declare module '*.bmp' {
18 | const src: string;
19 | export default src;
20 | }
21 |
22 | declare module '*.gif' {
23 | const src: string;
24 | export default src;
25 | }
26 |
27 | declare module '*.jpg' {
28 | const src: string;
29 | export default src;
30 | }
31 |
32 | declare module '*.jpeg' {
33 | const src: string;
34 | export default src;
35 | }
36 |
37 | declare module '*.png' {
38 | const src: string;
39 | export default src;
40 | }
41 |
42 | declare module '*.webp' {
43 | const src: string;
44 | export default src;
45 | }
46 |
47 | declare module '*.svg' {
48 | import * as React from 'react';
49 |
50 | export const ReactComponent: React.FunctionComponent<
51 | React.SVGProps & { title?: string }
52 | >;
53 |
54 | const src: string;
55 | export default src;
56 | }
57 |
58 | declare module '*.module.css' {
59 | const classes: { readonly [key: string]: string };
60 | export default classes;
61 | }
62 |
63 | declare module '*.module.scss' {
64 | const classes: { readonly [key: string]: string };
65 | export default classes;
66 | }
67 |
68 | declare module '*.module.sass' {
69 | const classes: { readonly [key: string]: string };
70 | export default classes;
71 | }
72 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Echo
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/utils/RequestUtils.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import config from './../../config.json';
3 | import packageInfo from './../../package.json';
4 |
5 | const sha1 = require('js-sha1');
6 |
7 | const BASE_URL = 'https://api.podcastindex.org/api/1.0';
8 |
9 | const reqInstance = () => {
10 | const unixTime = new Date().getTime().toString().slice(0, -3);
11 | const { podcastIndexAPIKey, podcastIndexSecretKey } = config;
12 | const hash = sha1(
13 | `${podcastIndexAPIKey}${podcastIndexSecretKey}${unixTime}`
14 | );
15 |
16 | return axios.create({
17 | headers: {
18 | Authorization: hash,
19 | 'X-Auth-Key': podcastIndexAPIKey,
20 | 'X-Auth-Date': unixTime,
21 | 'User-Agent': `Echo v${packageInfo.version}`
22 | }
23 | });
24 | };
25 |
26 | const searchPodcasts = (searchString = '') => {
27 | return reqInstance()
28 | .get(`${BASE_URL}/search/byterm?q=${searchString}`)
29 | .then((res: any) => {
30 | const shows = res.data.feeds;
31 | return shows;
32 | })
33 | .catch((err: Error) => {
34 | console.log('Error: ', err.message);
35 | });
36 | };
37 |
38 | const podcastByFeedId = (searchString = '') => {
39 | return reqInstance()
40 | .get(`${BASE_URL}/podcasts/byfeedid?id=${searchString}`)
41 | .then((res: any) => {
42 | const show = res.data.feed;
43 | return show;
44 | })
45 | .catch((err: Error) => {
46 | console.log('Error: ', err.message);
47 | });
48 | };
49 |
50 | const episodesByFeedId = (searchString = '') => {
51 | return reqInstance()
52 | .get(`${BASE_URL}/episodes/byfeedid?id=${searchString}`)
53 | .then((res: any) => {
54 | const episodes = res.data.items;
55 | return episodes;
56 | })
57 | .catch((err: Error) => {
58 | console.log('Error: ', err.message);
59 | });
60 | };
61 |
62 | export { searchPodcasts, podcastByFeedId, episodesByFeedId };
63 |
--------------------------------------------------------------------------------
/config/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const crypto = require('crypto');
6 | const chalk = require('react-dev-utils/chalk');
7 | const paths = require('./paths');
8 |
9 | // Ensure the certificate and key provided are valid and if not
10 | // throw an easy to debug error
11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
12 | let encrypted;
13 | try {
14 | // publicEncrypt will throw an error with an invalid cert
15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
16 | } catch (err) {
17 | throw new Error(
18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
19 | );
20 | }
21 |
22 | try {
23 | // privateDecrypt will throw an error with an invalid key
24 | crypto.privateDecrypt(key, encrypted);
25 | } catch (err) {
26 | throw new Error(
27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
28 | err.message
29 | }`
30 | );
31 | }
32 | }
33 |
34 | // Read file and throw an error if it doesn't exist
35 | function readEnvFile(file, type) {
36 | if (!fs.existsSync(file)) {
37 | throw new Error(
38 | `You specified ${chalk.cyan(
39 | type
40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
41 | );
42 | }
43 | return fs.readFileSync(file);
44 | }
45 |
46 | // Get the https config
47 | // Return cert files if provided in env, otherwise just true or false
48 | function getHttpsConfig() {
49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
50 | const isHttps = HTTPS === 'true';
51 |
52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
55 | const config = {
56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
58 | };
59 |
60 | validateKeyAndCerts({ ...config, keyFile, crtFile });
61 | return config;
62 | }
63 | return isHttps;
64 | }
65 |
66 | module.exports = getHttpsConfig;
67 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
13 | // "public path" at which the app is served.
14 | // webpack needs to know it to put the right