diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0bd2cb5
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "client/submodules/youtube-music-api"]
+ path = client/submodules/youtube-music-api
+ url = https://github.com/SanderVocke/youtube-music-api.git
+[submodule "client/src/submodules/youtube-music-api"]
+ path = client/src/submodules/youtube-music-api
+ url = https://github.com/SanderVocke/youtube-music-api.git
diff --git a/client/package-lock.json b/client/package-lock.json
index b8c35fe..da974b8 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1837,6 +1837,14 @@
"hoist-non-react-statics": "^3.3.0"
}
},
+ "@types/http-proxy": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
+ "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -2732,6 +2740,14 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz",
"integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA=="
},
+ "axios": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
+ "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
+ "requires": {
+ "follow-redirects": "^1.10.0"
+ }
+ },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -6905,14 +6921,55 @@
}
},
"http-proxy-middleware": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
- "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
+ "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
"requires": {
- "http-proxy": "^1.17.0",
- "is-glob": "^4.0.0",
- "lodash": "^4.17.11",
- "micromatch": "^3.1.10"
+ "@types/http-proxy": "^1.17.4",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.20",
+ "micromatch": "^4.0.2"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ }
}
},
"http-signature": {
@@ -14106,6 +14163,17 @@
}
}
},
+ "http-proxy-middleware": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
+ "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+ "requires": {
+ "http-proxy": "^1.17.0",
+ "is-glob": "^4.0.0",
+ "lodash": "^4.17.11",
+ "micromatch": "^3.1.10"
+ }
+ },
"is-absolute-url": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
diff --git a/client/package.json b/client/package.json
index 1fb3ac0..3e23ba5 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,6 +16,8 @@
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5",
"@types/uuid": "^8.3.0",
+ "axios": "^0.21.0",
+ "http-proxy-middleware": "^1.0.6",
"jsurl": "^0.1.5",
"lodash": "^4.17.20",
"material-table": "^1.69.0",
@@ -49,6 +51,5 @@
"last 1 firefox version",
"last 1 safari version"
]
- },
- "proxy": "http://localhost:5000/"
+ }
}
diff --git a/client/src/assets/youtubemusic_icon.svg b/client/src/assets/youtubemusic_icon.svg
new file mode 100644
index 0000000..97f8ee6
--- /dev/null
+++ b/client/src/assets/youtubemusic_icon.svg
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx
index d600867..df71822 100644
--- a/client/src/components/MainWindow.tsx
+++ b/client/src/components/MainWindow.tsx
@@ -1,4 +1,4 @@
-import React, { useReducer, Reducer } from 'react';
+import React, { useReducer, Reducer, useEffect } from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core';
import { grey } from '@material-ui/core/colors';
import AppBar from './appbar/AppBar';
@@ -10,6 +10,8 @@ import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
+import Integration from '../integrations/Integration';
+import YoutubeMusicIntegration from '../integrations/youtube_music/YoutubeMusicIntegration';
var _ = require('lodash');
const darkTheme = createMuiTheme({
@@ -26,6 +28,7 @@ export interface MainWindowState {
tabReducers: Reducer[],
tabTypes: WindowType[],
activeTab: number,
+ activeIntegrations: Integration[],
}
export enum MainWindowStateActions {
@@ -33,6 +36,7 @@ export enum MainWindowStateActions {
DispatchToTab = "dispatchToTab",
CloseTab = "closeTab",
AddTab = "addTab",
+ AddIntegration = "addIntegration",
}
export function MainWindowReducer(state: MainWindowState, action: any) {
@@ -64,6 +68,11 @@ export function MainWindowReducer(state: MainWindowState, action: any) {
item;
})
}
+ case MainWindowStateActions.AddIntegration:
+ return {
+ ...state,
+ activeIntegrations: [ ...state.activeIntegrations, action.value ]
+ }
default:
throw new Error("Unimplemented MainWindow state update.")
}
@@ -95,9 +104,20 @@ export default function MainWindow(props: any) {
WindowType.Tag,
WindowType.ManageTags,
],
- activeTab: 0
+ activeTab: 0,
+ activeIntegrations: [],
})
+ useEffect(() => {
+ const integration = new YoutubeMusicIntegration();
+ integration.connect({}).then(() => {
+ dispatch({
+ type: MainWindowStateActions.AddIntegration,
+ value: integration,
+ })
+ })
+ }, []);
+
const windows = state.tabStates.map((tabState: any, i: number) => {
const tabDispatch = (action: any) => {
dispatch({
@@ -164,6 +184,7 @@ export default function MainWindow(props: any) {
tabType: w.windowType,
})
}}
+ integrations={state.activeIntegrations}
/>
{windows[state.activeTab]}
diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx
index 820b479..7ef1773 100644
--- a/client/src/components/appbar/AppBar.tsx
+++ b/client/src/components/appbar/AppBar.tsx
@@ -3,6 +3,8 @@ import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton } from '@mate
import CloseIcon from '@material-ui/icons/Close';
import AddIcon from '@material-ui/icons/Add';
import AddTabMenu, { NewTabProps } from './AddTabMenu';
+import Integration from '../../integrations/Integration';
+import IntegrationsWidget from './IntegrationsWidget';
export interface IProps {
tabLabels: string[],
@@ -10,6 +12,7 @@ export interface IProps {
setSelectedTab: (n: number) => void,
onCloseTab: (idx: number) => void,
onAddTab: (w: NewTabProps) => void,
+ integrations: Integration[],
}
export interface TabProps {
@@ -61,6 +64,9 @@ export default function AppBar(props: IProps) {
+
+
+
props.setSelectedTab(v)}
diff --git a/client/src/components/appbar/IntegrationsWidget.tsx b/client/src/components/appbar/IntegrationsWidget.tsx
new file mode 100644
index 0000000..6367894
--- /dev/null
+++ b/client/src/components/appbar/IntegrationsWidget.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import Integration from '../../integrations/Integration';
+
+export default function IntegrationsWidget(props: {
+ integrations: Integration[]
+}) {
+ return <>
+ {props.integrations.map((integration: Integration) => {
+ return integration.getIcon({ style: { height: '30px', width: '30px' } });
+ })}
+ >
+}
\ No newline at end of file
diff --git a/client/src/components/common/StoreLinkIcon.tsx b/client/src/components/common/StoreLinkIcon.tsx
index 7359ab0..78b05c8 100644
--- a/client/src/components/common/StoreLinkIcon.tsx
+++ b/client/src/components/common/StoreLinkIcon.tsx
@@ -1,8 +1,10 @@
import React from 'react';
import { ReactComponent as GPMIcon } from '../../assets/googleplaymusic_icon.svg';
+import { ReactComponent as YTMIcon } from '../../assets/youtubemusic_icon.svg';
export enum ExternalStore {
- GooglePlayMusic = "GPM",
+ GooglePlayMusic = "GooglePlayMusic",
+ YoutubeMusic = "YoutubeMusic",
}
export interface IProps {
@@ -10,9 +12,12 @@ export interface IProps {
}
export function whichStore(url: string) {
- if(url.includes('play.google.com')) {
+ if(url.includes('play.google.com') || url.includes('music.google.com')) {
return ExternalStore.GooglePlayMusic;
}
+ if(url.includes('music.youtube.com')) {
+ return ExternalStore.YoutubeMusic;
+ }
return undefined;
}
@@ -22,6 +27,8 @@ export default function StoreLinkIcon(props: any) {
switch(whichStore) {
case ExternalStore.GooglePlayMusic:
return ;
+ case ExternalStore.YoutubeMusic:
+ return
default:
throw new Error("Unknown external store: " + whichStore)
}
diff --git a/client/src/integrations/Integration.tsx b/client/src/integrations/Integration.tsx
new file mode 100644
index 0000000..a67781b
--- /dev/null
+++ b/client/src/integrations/Integration.tsx
@@ -0,0 +1,52 @@
+import React, { ReactFragment } from 'react';
+
+export interface IntegrationAlbum {
+ name?: string,
+ storeLink?: string,
+}
+
+export interface IntegrationArtist {
+ name?: string,
+ storeLink?: string,
+}
+
+export interface IntegrationSong {
+ title?: string,
+ album?: IntegrationAlbum,
+ artist?: IntegrationArtist,
+ storeLink?: string,
+}
+
+export enum IntegrationFeature {
+ // Used to get a bucket of songs (typically: the whole library)
+ GetSongs = 0,
+
+ // Used to search songs and get some amount of candidate results.
+ SearchSong,
+
+ // If included, the integration is required to be connected to work.
+ // Methods connect() and isConnected() have to be used for this.
+ UsesConnection,
+}
+
+export interface IntegrationDescriptor {
+ supports: IntegrationFeature[],
+}
+
+export default class Integration {
+ constructor() { }
+
+ // Common
+ getFeatures(): IntegrationFeature[] { return []; }
+ getIcon(props: any): ReactFragment { return <>> }
+
+ // Feature: UsesConnection
+ async connect(connectParams: any) { }
+ isConnected(): boolean { return false; }
+
+ // Feature: GetSongs
+ async getSongs(getSongsParams: any): Promise { return []; }
+
+ // Feature: SearchSongs
+ async searchSong(songProps: IntegrationSong): Promise { return null; }
+}
\ No newline at end of file
diff --git a/client/src/integrations/youtube_music/YoutubeMusicIntegration.tsx b/client/src/integrations/youtube_music/YoutubeMusicIntegration.tsx
new file mode 100644
index 0000000..674bf1a
--- /dev/null
+++ b/client/src/integrations/youtube_music/YoutubeMusicIntegration.tsx
@@ -0,0 +1,55 @@
+
+import React from 'react';
+import Integration, { IntegrationFeature, IntegrationSong } from "../Integration";
+import StoreLinkIcon, { ExternalStore } from "../../components/common/StoreLinkIcon";
+const YoutubeMusicApi = require('../../submodules/youtube-music-api');
+
+export interface YoutubeMusicConnectionParams {}
+
+export default class YoutubeMusicIntegration extends Integration {
+ lastConnected: Date | null = null;
+ connectionIntervalMs: number = 1000 * 60 * 2; // 2 minutes
+ connectionParams: YoutubeMusicConnectionParams | null = null;
+ api: any = null;
+
+ getFeatures() : IntegrationFeature[] {
+ return [
+ IntegrationFeature.SearchSong,
+ IntegrationFeature.UsesConnection,
+ ]
+ }
+
+ getIcon(props: any) {
+ return
+ }
+
+ async connect(connectionParams: YoutubeMusicConnectionParams) {
+ const newApi = new YoutubeMusicApi();
+ await newApi.initalize();
+ this.api = newApi;
+ this.lastConnected = new Date();
+
+ // TODO start keepalive service worker?
+ }
+
+ isConnected() {
+ if(!this.connectionParams || !this.lastConnected) {
+ return false;
+ }
+
+ // Last successful connection must be less than 2 minutes ago.
+ return ((new Date().valueOf()) - this.lastConnected.valueOf()) < this.connectionIntervalMs;
+ }
+
+ async searchSong(songProps: IntegrationSong) {
+ var query: string = "";
+ if(songProps.title) { query += " " + songProps.title }
+ if(songProps.artist && songProps.artist.name) { query += " " + songProps.artist.name }
+
+ const result = await this.api.search(query.trim(), "song");
+
+ console.log("Search result: ", result)
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js
new file mode 100644
index 0000000..aad8601
--- /dev/null
+++ b/client/src/setupProxy.js
@@ -0,0 +1,18 @@
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(
+ '/api',
+ createProxyMiddleware({
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ })
+ );
+ app.use(
+ '/integrations/ytm',
+ createProxyMiddleware({
+ target: 'https://music.youtube.com',
+ changeOrigin: true,
+ })
+ );
+};
\ No newline at end of file
diff --git a/client/src/submodules/youtube-music-api b/client/src/submodules/youtube-music-api
new file mode 160000
index 0000000..0184b80
--- /dev/null
+++ b/client/src/submodules/youtube-music-api
@@ -0,0 +1 @@
+Subproject commit 0184b80d4b36f92b2874a0aece6e01d92af0c92c