Compare commits

...

1 Commits

Author SHA1 Message Date
Sander Vocke dff4acab25 Get YoutubeMusciApi working from the client using a proxy. 5 years ago
  1. 6
      .gitmodules
  2. 82
      client/package-lock.json
  3. 5
      client/package.json
  4. 16
      client/src/assets/youtubemusic_icon.svg
  5. 25
      client/src/components/MainWindow.tsx
  6. 6
      client/src/components/appbar/AppBar.tsx
  7. 12
      client/src/components/appbar/IntegrationsWidget.tsx
  8. 11
      client/src/components/common/StoreLinkIcon.tsx
  9. 52
      client/src/integrations/Integration.tsx
  10. 55
      client/src/integrations/youtube_music/YoutubeMusicIntegration.tsx
  11. 18
      client/src/setupProxy.js
  12. 1
      client/src/submodules/youtube-music-api

6
.gitmodules vendored

@ -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

@ -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",

@ -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/"
}
}

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 176 176" enable-background="new 0 0 176 176" xml:space="preserve">
<metadata>
<sfw xmlns="http://ns.adobe.com/SaveForWeb/1.0/">
<slices/>
<sliceSourceBounds bottomLeftOrigin="true" height="176" width="176" x="8" y="-184"/>
</sfw>
</metadata>
<g id="XMLID_167_">
<circle id="XMLID_791_" fill="#FF0000" cx="88" cy="88" r="88"/>
<path id="XMLID_42_" fill="#FFFFFF" d="M88,46c23.1,0,42,18.8,42,42s-18.8,42-42,42s-42-18.8-42-42S64.9,46,88,46 M88,42 c-25.4,0-46,20.6-46,46s20.6,46,46,46s46-20.6,46-46S113.4,42,88,42L88,42z"/>
<polygon id="XMLID_274_" fill="#FFFFFF" points="72,111 111,87 72,65 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 696 B

After

Width:  |  Height:  |  Size: 944 B

@ -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<any, any>[],
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]}
</ThemeProvider>

@ -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) {
<Box m={0.5} display="flex" alignItems="center">
<img height="30px" src={process.env.PUBLIC_URL + "/logo.svg"} alt="error"></img>
</Box>
<Box m={0.5} display="flex" alignItems="center">
<IntegrationsWidget integrations={props.integrations}/>
</Box>
<Tabs
value={props.selectedTab}
onChange={(e: any, v: number) => props.setSelectedTab(v)}

@ -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' } });
})}
</>
}

@ -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 <GPMIcon {...restProps}/>;
case ExternalStore.YoutubeMusic:
return <YTMIcon {...restProps}/>
default:
throw new Error("Unknown external store: " + whichStore)
}

@ -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<IntegrationSong[]> { return []; }
// Feature: SearchSongs
async searchSong(songProps: IntegrationSong): Promise<IntegrationSong | null> { return null; }
}

@ -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 <StoreLinkIcon whichStore={ExternalStore.YoutubeMusic} {...props}/>
}
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;
}
}

@ -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,
})
);
};

@ -0,0 +1 @@
Subproject commit 0184b80d4b36f92b2874a0aece6e01d92af0c92c
Loading…
Cancel
Save