mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
New:
- Lyric seek with continuos pop up on track change - Settings (UI only) - Switch (as toggler) Modified: - Home - PlaylistGenreView - TabMenu All above was modified to adjust compatibility with Lyric View & Settings
This commit is contained in:
parent
87bd1a9abd
commit
09467e6e9a
1
assets/music-solid.svg
Normal file
1
assets/music-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="music" class="svg-inline--fa fa-music fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M470.38 1.51L150.41 96A32 32 0 0 0 128 126.51v261.41A139 139 0 0 0 96 384c-53 0-96 28.66-96 64s43 64 96 64 96-28.66 96-64V214.32l256-75v184.61a138.4 138.4 0 0 0-32-3.93c-53 0-96 28.66-96 64s43 64 96 64 96-28.65 96-64V32a32 32 0 0 0-41.62-30.49z"></path></svg>
|
After Width: | Height: | Size: 474 B |
1
assets/setting-cog.svg
Normal file
1
assets/setting-cog.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="cog" class="svg-inline--fa fa-cog fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z"></path></svg>
|
After Width: | Height: | Size: 1.0 KiB |
1
assets/times-solid.svg
Normal file
1
assets/times-solid.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>
|
After Width: | Height: | Size: 645 B |
2083
package-lock.json
generated
2083
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -9,6 +9,7 @@
|
||||
"build": "webpack --mode=production",
|
||||
"dev": "cross-env TSC_WATCHFILE=UseFsEvents webpack --mode=development",
|
||||
"start": "qode ./dist/index.js",
|
||||
"start-dev": "concurrently -n 'webpack,spotube' -p '{name}-{pid}' -c 'bgBlue,bgGreen' -i --default-input-target spotube 'npm run dev' 'nodemon -e node -w ./*.babelrc -x \"npm start\"'",
|
||||
"start:trace": "qode ./dist/index.js --trace",
|
||||
"debug": "qode --inspect ./dist/index.js",
|
||||
"pack": "nodegui-packer -p ./dist",
|
||||
@ -25,6 +26,7 @@
|
||||
"dotenv": "^8.2.0",
|
||||
"du": "^1.0.0",
|
||||
"express": "^4.17.1",
|
||||
"html-to-text": "^7.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"jimp": "^0.16.1",
|
||||
"node-localstorage": "^2.1.6",
|
||||
@ -39,14 +41,16 @@
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/core": "^7.13.10",
|
||||
"@babel/preset-env": "^7.13.12",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@nodegui/devtools": "^1.0.1",
|
||||
"@nodegui/packer": "^1.4.1",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/du": "^1.0.0",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/html-to-text": "^6.0.0",
|
||||
"@types/is-url": "^1.2.28",
|
||||
"@types/node": "^14.11.1",
|
||||
"@types/node-localstorage": "^1.3.0",
|
||||
@ -57,10 +61,12 @@
|
||||
"@types/webpack-env": "^1.15.3",
|
||||
"babel-loader": "^8.1.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.0",
|
||||
"native-addon-loader": "^2.0.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"typescript": "^4.2.3",
|
||||
"webpack": "^5.27.0",
|
||||
"webpack-cli": "^4.4.0"
|
||||
|
@ -15,8 +15,8 @@ function Home() {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
|
||||
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||
<ScrollArea style={`flex-grow: 1; border: none;`}>
|
||||
<View style={`flex-direction: 'column'; align-items: 'center'; flex: 1;`}>
|
||||
<PlaceholderApplet error={isError} message="Failed to query genres" reload={refetch} helps loading={isLoading} />
|
||||
{categories?.map((category, index) => {
|
||||
return <CategoryCard key={index + category.id} id={category.id} name={category.name} />;
|
||||
@ -36,9 +36,8 @@ interface CategoryCardProps {
|
||||
const categoryStylesheet = `
|
||||
#container{
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
flex-direction: 'column';
|
||||
justify-content: 'center';
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
#anchor-heading{
|
||||
background: transparent;
|
||||
@ -51,8 +50,6 @@ const categoryStylesheet = `
|
||||
}
|
||||
#child-view{
|
||||
flex: 1;
|
||||
justify-content: 'space-around';
|
||||
align-items: 'center';
|
||||
}
|
||||
#anchor-heading:hover{
|
||||
border: none;
|
||||
|
@ -4,19 +4,17 @@ import { Redirect, Route } from "react-router";
|
||||
import { QueryCacheKeys } from "../conf";
|
||||
import playerContext from "../context/playerContext";
|
||||
import useSpotifyInfiniteQuery from "../hooks/useSpotifyInfiniteQuery";
|
||||
import useSpotifyQuery from "../hooks/useSpotifyQuery";
|
||||
import { GenreView } from "./PlaylistGenreView";
|
||||
import { PlaylistSimpleControls, TrackTableIndex } from "./PlaylistView";
|
||||
import PlaceholderApplet from "./shared/PlaceholderApplet";
|
||||
import PlaylistCard from "./shared/PlaylistCard";
|
||||
import { TrackButton, TrackButtonPlaylistObject } from "./shared/TrackButton";
|
||||
import { TabMenuItem } from "./TabMenu";
|
||||
|
||||
function Library() {
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'row';">
|
||||
<View style="flex: 1; flex-direction: 'column';">
|
||||
<Redirect from="/library" to="/library/saved-tracks" />
|
||||
<View style="flex-direction: 'column'; flex: 1; max-width: 150px;">
|
||||
<View style="max-width: 350px; justify-content: 'space-evenly'">
|
||||
<TabMenuItem title="Saved Tracks" url="/library/saved-tracks" />
|
||||
<TabMenuItem title="Playlists" url="/library/playlists" />
|
||||
</View>
|
||||
|
61
src/components/ManualLyricDialog.tsx
Normal file
61
src/components/ManualLyricDialog.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { FlexLayout, QDialog, QLabel, QScrollArea, QWidget, TextFormat } from "@nodegui/nodegui";
|
||||
import React, { PropsWithChildren, useEffect, useState } from "react";
|
||||
import showError from "../helpers/showError";
|
||||
import fetchLyrics from "../helpers/fetchLyrics";
|
||||
|
||||
interface ManualLyricDialogProps extends PropsWithChildren<{}> {
|
||||
open: boolean;
|
||||
onClose?: (closed:boolean) => void;
|
||||
track: SpotifyApi.TrackObjectSimplified | SpotifyApi.TrackObjectFull;
|
||||
}
|
||||
|
||||
function ManualLyricDialog({ open, track, onClose }: ManualLyricDialogProps) {
|
||||
const dialog = new QDialog();
|
||||
const areaContainer = new QWidget();
|
||||
const scrollArea = new QScrollArea();
|
||||
const titleLabel = new QLabel();
|
||||
const lyricLabel = new QLabel();
|
||||
const [lyrics, setLyrics] = useState<string>("");
|
||||
const artists = track.artists.map((artist) => artist.name).join(", ");
|
||||
|
||||
useEffect(() => {
|
||||
// title label
|
||||
titleLabel.setText(`
|
||||
<center>
|
||||
<h2>${track.name}</h2>
|
||||
<p>- ${artists}</p>
|
||||
</center>
|
||||
`);
|
||||
// lyric label
|
||||
lyricLabel.setText(lyrics);
|
||||
lyricLabel.setTextFormat(TextFormat.PlainText);
|
||||
// area container
|
||||
areaContainer.setLayout(new FlexLayout());
|
||||
areaContainer.setInlineStyle("flex: 1; flex-direction: 'column'; padding: 10px;");
|
||||
areaContainer.layout?.addWidget(titleLabel);
|
||||
areaContainer.layout?.addWidget(lyricLabel);
|
||||
// scroll area
|
||||
scrollArea.setInlineStyle("flex: 1;");
|
||||
scrollArea.setWidget(areaContainer);
|
||||
// dialog
|
||||
dialog.setWindowTitle("Lyrics");
|
||||
dialog.setLayout(new FlexLayout());
|
||||
dialog.layout?.addWidget(scrollArea);
|
||||
open ? dialog.open() : dialog.close();
|
||||
open && fetchLyrics(artists, track.name)
|
||||
.then((lyrics: string) => {
|
||||
setLyrics(lyrics);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
showError(e, `[Finding lyrics for ${track.name} failed]: `);
|
||||
setLyrics("No lyrics found, rare track :)");
|
||||
});
|
||||
return () => {
|
||||
dialog.hide()
|
||||
}
|
||||
}, [open, track, lyrics]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default ManualLyricDialog;
|
@ -6,10 +6,11 @@ import { shuffleArray } from "../helpers/shuffleArray";
|
||||
import NodeMpv from "node-mpv";
|
||||
import { getYoutubeTrack } from "../helpers/getYoutubeTrack";
|
||||
import PlayerProgressBar from "./PlayerProgressBar";
|
||||
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart } from "../icons";
|
||||
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart, musicNode } from "../icons";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import showError from "../helpers/showError";
|
||||
import useTrackReaction from "../hooks/useTrackReaction";
|
||||
import ManualLyricDialog from "./ManualLyricDialog";
|
||||
|
||||
export const audioPlayer = new NodeMpv(
|
||||
{
|
||||
@ -22,7 +23,6 @@ export const audioPlayer = new NodeMpv(
|
||||
},
|
||||
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
|
||||
);
|
||||
|
||||
function Player(): ReactElement {
|
||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||
@ -32,6 +32,7 @@ function Player(): ReactElement {
|
||||
const [shuffle, setShuffle] = useState<boolean>(false);
|
||||
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
||||
const [isStopped, setIsStopped] = useState<boolean>(false);
|
||||
const [openLyrics, setOpenLyrics] = useState<boolean>(false);
|
||||
const playlistTracksIds = currentPlaylist?.tracks.map((t) => t.track.id);
|
||||
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
||||
{
|
||||
@ -91,7 +92,7 @@ function Player(): ReactElement {
|
||||
// changing shuffle to default
|
||||
useEffect(() => {
|
||||
setShuffle(false);
|
||||
}, [currentPlaylist])
|
||||
}, [currentPlaylist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (playerRunning) {
|
||||
@ -198,6 +199,7 @@ function Player(): ReactElement {
|
||||
</GridColumn>
|
||||
<GridColumn width={4}>
|
||||
<BoxView direction={Direction.TopToBottom} style={`max-width: 600px; min-width: 380px;`}>
|
||||
{currentTrack && <ManualLyricDialog open={openLyrics} track={currentTrack} />}
|
||||
<PlayerProgressBar audioPlayer={audioPlayer} totalDuration={totalDuration} />
|
||||
|
||||
<BoxView direction={Direction.LeftToRight}>
|
||||
@ -221,6 +223,11 @@ function Player(): ReactElement {
|
||||
}}
|
||||
icon={new QIcon(isFavorite(currentTrack?.id ?? "") ? heart : heartRegular)}
|
||||
/>
|
||||
<IconButton
|
||||
style={openLyrics ? "background-color: green;": ""}
|
||||
icon={new QIcon(musicNode)}
|
||||
on={{ clicked: () => currentTrack && setOpenLyrics(!openLyrics) }}
|
||||
/>
|
||||
<Slider minSize={{ height: 20, width: 80 }} maxSize={{ height: 20, width: 100 }} hasTracking sliderPosition={volume} on={volumeHandler} orientation={Orientation.Horizontal} />
|
||||
</BoxView>
|
||||
</GridColumn>
|
||||
|
@ -35,14 +35,13 @@ interface GenreViewProps {
|
||||
export function GenreView({ heading, playlists, loadMore, isLoadable, isError, isLoading, refetch }: GenreViewProps) {
|
||||
const playlistGenreViewStylesheet = `
|
||||
#genre-container{
|
||||
flex-direction: column;
|
||||
flex-direction: 'column';
|
||||
flex: 1;
|
||||
}
|
||||
#heading {
|
||||
padding: 10px;
|
||||
}
|
||||
#scroll-view{
|
||||
flex: 1;
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
}
|
||||
|
36
src/components/Settings.tsx
Normal file
36
src/components/Settings.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Text, View } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import Switch from "./shared/Switch";
|
||||
|
||||
function Settings() {
|
||||
return (
|
||||
<View style="flex: 1; flex-direction: 'column'; justify-content: 'flex-start';">
|
||||
<Text>{`<center><h2>Settings</h2></center>`}</Text>
|
||||
<View style="width: '100%'; flex-direction: 'column'; justify-content: 'flex-start';">
|
||||
<SettingsCheckTile title="Use images instead of colors for playlist" subtitle="This will increase memory usage" />
|
||||
<SettingsCheckTile title="Some unknown settings" />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
||||
interface SettingsCheckTileProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export function SettingsCheckTile({ title, subtitle = "" }: SettingsCheckTileProps) {
|
||||
return (
|
||||
<View style="flex: 1; align-items: 'center'; padding: 15px 0; justify-content: 'space-between';">
|
||||
<Text>
|
||||
{`
|
||||
<b>${title}</b>
|
||||
<p>${subtitle}</p>
|
||||
`}
|
||||
</Text>
|
||||
<Switch checked/>
|
||||
</View>
|
||||
);
|
||||
}
|
@ -1,8 +1,12 @@
|
||||
import React from "react";
|
||||
import { View, Button, Text } from "@nodegui/react-nodegui";
|
||||
import { useHistory, useLocation } from "react-router";
|
||||
import { settingsCog } from "../icons";
|
||||
import IconButton from "./shared/IconButton";
|
||||
import { QIcon } from "@nodegui/nodegui";
|
||||
|
||||
function TabMenu() {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<View id="tabmenu" styleSheet={tabBarStylesheet}>
|
||||
@ -13,6 +17,14 @@ function TabMenu() {
|
||||
<TabMenuItem url="/library" title="Library" />
|
||||
<TabMenuItem url="/currently" title="Currently Playing" />
|
||||
<TabMenuItem url="/search" title="Search" />
|
||||
<IconButton
|
||||
icon={new QIcon(settingsCog)}
|
||||
on={{
|
||||
clicked() {
|
||||
history.push("/settings");
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
42
src/components/shared/Switch.tsx
Normal file
42
src/components/shared/Switch.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Orientation, QMouseEvent } from "@nodegui/nodegui";
|
||||
import { Slider } from "@nodegui/react-nodegui";
|
||||
import { CheckBoxProps } from "@nodegui/react-nodegui/dist/components/CheckBox/RNCheckBox";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface SwitchProps extends Omit<CheckBoxProps, "on" |"icon" | "text">{
|
||||
onChange?(checked:boolean): void
|
||||
}
|
||||
|
||||
function Switch({ checked, onChange, ...props }: SwitchProps) {
|
||||
const [value, setValue] = useState<0|1>(0);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(checked ? 1 : 0);
|
||||
}, [checked])
|
||||
|
||||
return (
|
||||
<Slider
|
||||
value={value}
|
||||
hasTracking
|
||||
mouseTracking
|
||||
orientation={Orientation.Horizontal}
|
||||
maximum={1}
|
||||
minimum={0}
|
||||
maxSize={{ width: 30, height: 20 }}
|
||||
on={{
|
||||
valueChanged(value) {
|
||||
onChange && onChange(value === 1);
|
||||
},
|
||||
MouseButtonRelease(native: any) {
|
||||
const mouse = new QMouseEvent(native);
|
||||
if (mouse.button() === 1) {
|
||||
setValue(value===1?0:1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{...props}
|
||||
/>);
|
||||
}
|
||||
|
||||
export default Switch;
|
41
src/helpers/fetchLyrics.ts
Normal file
41
src/helpers/fetchLyrics.ts
Normal file
@ -0,0 +1,41 @@
|
||||
|
||||
import axios from 'axios';
|
||||
import htmlToText from 'html-to-text';
|
||||
const delim1 = '</div></div></div></div><div class="hwc"><div class="BNeawe tAd8D AP7Wnd"><div><div class="BNeawe tAd8D AP7Wnd">';
|
||||
const delim2 = '</div></div></div></div></div><div><span class="hwc"><div class="BNeawe uEec3 AP7Wnd">';
|
||||
const url = "https://www.google.com/search?q=";
|
||||
|
||||
export default async function fetchLyrics(artists:string, title: string) {
|
||||
let lyrics;
|
||||
try {
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+lyrics`, {responseType: "text"})).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (m) {
|
||||
try {
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song+lyrics`)).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (n) {
|
||||
try {
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}+song`)).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (o) {
|
||||
try {
|
||||
lyrics = (await axios.get<string>(`${url}${encodeURIComponent(title + " " + artists)}`)).data;
|
||||
[, lyrics] = lyrics.split(delim1);
|
||||
[lyrics] = lyrics.split(delim2);
|
||||
} catch (p) {
|
||||
lyrics = 'Not Found';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const rets = lyrics.split('\n');
|
||||
let final = '';
|
||||
for (const ret of rets) {
|
||||
final = `${final}${htmlToText.htmlToText(ret)}\n`;
|
||||
}
|
||||
return final.trim();
|
||||
}
|
@ -26,7 +26,7 @@ export async function getYoutubeTrack(track: SpotifyApi.TrackObjectFull): Promis
|
||||
try {
|
||||
const artistsName = track.artists.map((ar) => ar.name);
|
||||
const queryString = `${artistsName[0]} - ${track.name}${artistsName.length > 1 ? ` feat. ${artistsName.slice(1).join(" ")}` : ``}`;
|
||||
console.log('Youtube Query String:', queryString);
|
||||
console.log("Youtube Query String:", queryString);
|
||||
const result = await scrapYt.search(queryString, { limit: 7, type: "video" });
|
||||
const tracksWithRelevance = result
|
||||
.map((video) => {
|
||||
@ -40,7 +40,9 @@ export async function getYoutubeTrack(track: SpotifyApi.TrackObjectFull): Promis
|
||||
.sort((a, b) => (a.matchPercentage > b.matchPercentage ? -1 : 1));
|
||||
const sameChannelTracks = tracksWithRelevance.filter((tr) => tr.sameChannel);
|
||||
|
||||
const finalTrack = { ...track, youtube_uri: (sameChannelTracks.length > 0 ? sameChannelTracks : tracksWithRelevance)[0].url };
|
||||
const rarestTrack = result.map((res) => ({ url: `http://www.youtube.com/watch?v=${res.id}`, id: res.id }));
|
||||
|
||||
const finalTrack = { ...track, youtube_uri: (sameChannelTracks.length > 0 ? sameChannelTracks : tracksWithRelevance.length > 0 ? tracksWithRelevance : rarestTrack)[0].url };
|
||||
return finalTrack;
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve track's youtube url: ", error);
|
||||
|
@ -9,6 +9,9 @@ import _random from "../assets/random-solid.svg"
|
||||
import _stop from "../assets/stop-solid.svg"
|
||||
import _search from "../assets/search-solid.svg";
|
||||
import _loadingSpinner from "../assets/loading-spinner.gif";
|
||||
import _settingsCog from "../assets/setting-cog.svg"
|
||||
import _times from "../assets/times-solid.svg"
|
||||
import _musicNode from "../assets/music-solid.svg"
|
||||
|
||||
export const play = _play;
|
||||
export const pause = _pause;
|
||||
@ -21,3 +24,6 @@ export const random = _random;
|
||||
export const stop = _stop;
|
||||
export const search = _search;
|
||||
export const loadingSpinner = _loadingSpinner;
|
||||
export const settingsCog = _settingsCog;
|
||||
export const times = _times;
|
||||
export const musicNode = _musicNode;
|
@ -2,8 +2,14 @@ import { Renderer } from "@nodegui/react-nodegui";
|
||||
import React from "react";
|
||||
import App from "./app";
|
||||
|
||||
process.title = "My NodeGui App";
|
||||
Renderer.render(<App />);
|
||||
process.title = "Spotube";
|
||||
Renderer.render(<App />, {
|
||||
onInit(reconciler) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
require("@nodegui/devtools").connectReactDevtools(reconciler);
|
||||
}
|
||||
},
|
||||
});
|
||||
// This is for hot reloading (this will be stripped off in production by webpack)
|
||||
if (module.hot) {
|
||||
module.hot.accept(["./app"], function () {
|
||||
|
@ -11,6 +11,7 @@ import Library from "./components/Library";
|
||||
import Search from "./components/Search";
|
||||
import SearchResultPlaylistCollection from "./components/SearchResultPlaylistCollection";
|
||||
import SearchResultSongsCollection from "./components/SearchResultSongsCollection";
|
||||
import Settings from "./components/Settings";
|
||||
|
||||
function Routes() {
|
||||
const { isLoggedIn } = useContext(authContext);
|
||||
@ -50,6 +51,9 @@ function Routes() {
|
||||
<Route exact path="/search/songs">
|
||||
<SearchResultSongsCollection />
|
||||
</Route>
|
||||
<Route exact path="/settings/">
|
||||
<Settings />
|
||||
</Route>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user