mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
added track downloading feature & new screenshot of the app window
This commit is contained in:
parent
35da4d5fd4
commit
e4ef120687
14
README.md
14
README.md
@ -1,7 +1,7 @@
|
|||||||

|

|
||||||
|
|
||||||
Spotube is a [qt](https://qt.io) based lightweight spotify client which uses [nodegui/react-nodegui](https://github.com/nodegui/react-nodegui) as frontend & nodejs as backend. It utilizes the power of Spotify & Youtube's public API & creates a hazardless, performant & resource friendly User Experience
|
Spotube is a [qt](https://qt.io) based lightweight spotify client which uses [nodegui/react-nodegui](https://github.com/nodegui/react-nodegui) as frontend & nodejs as backend. It utilizes the power of Spotify & Youtube's public API & creates a hazardless, performant & resource friendly User Experience
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -23,10 +23,10 @@ Don't worry **spotify premium isn't required**😱. But some extra packages are
|
|||||||
- [MPV](https://mpv.io/installation/) player for playing the actual audio
|
- [MPV](https://mpv.io/installation/) player for playing the actual audio
|
||||||
- [youtube-dl](https://github.com/ytdl-org/youtube-dl) for streaming the audio from youtube. It already comes pre bundled with mpv
|
- [youtube-dl](https://github.com/ytdl-org/youtube-dl) for streaming the audio from youtube. It already comes pre bundled with mpv
|
||||||
|
|
||||||
**Tip!:** If you're using **[Windows]()** try installing **mpv & youtube-dl** player with **[chocolatey](https://chocolatey.org/install) package manager** as it'd make the installation a lot easier.
|
> **Tip!:** If you're using **[Windows]()** try installing **mpv & youtube-dl** player with **[chocolatey](https://chocolatey.org/install) package manager** as it'd make the installation a lot easier.
|
||||||
**But always install youtube-dl first & then mpv player**
|
**But always install youtube-dl first & then mpv player**
|
||||||
|
|
||||||
**Important for [Ubuntu/Debian]():** If you're using any **ubuntu/debian** based linux distro then **youtube-dl** installed from the typical **apt-get** repositories will most likely not work as that version is older than current release. So remove it & install from the repository manually
|
> **Important for [Ubuntu/Debian]():** If you're using any **ubuntu/debian** based linux distro then **youtube-dl** installed from the typical **apt-get** repositories will most likely not work as that version is older than current release. So remove it & install from the repository manually
|
||||||
|
|
||||||
Remove the **youtube-dl** installed with **mpv** player or from **apt package manger**
|
Remove the **youtube-dl** installed with **mpv** player or from **apt package manger**
|
||||||
|
|
||||||
@ -39,7 +39,6 @@ Now, Install youtube-dl from
|
|||||||
- official github repo: https://github.com/ytdl-org/youtube-dl#installation (recommended)
|
- official github repo: https://github.com/ytdl-org/youtube-dl#installation (recommended)
|
||||||
**or**
|
**or**
|
||||||
- snap installation
|
- snap installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ snap install youtube-dl
|
$ snap install youtube-dl
|
||||||
```
|
```
|
||||||
@ -75,7 +74,7 @@ You need a spotify account & a web app for
|
|||||||
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields
|
- Click on **SHOW CLIENT SECRET** to reveal the **clientSecret**. Then copy the **clientID**, **clientSecret** & paste in the **Spotube's** respective fields
|
||||||

|

|
||||||
|
|
||||||
**[Important]!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
|
> **Note!**: No personal data or any kind of sensitive information won't be collected from spotify. Don't believe? See the code for yourself
|
||||||
|
|
||||||
### Building from source
|
### Building from source
|
||||||
|
|
||||||
@ -146,7 +145,7 @@ $ npm start
|
|||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
There will be some glitches, lags & stuck motions because of the library Spotube is currently using under the hood. It has some issues with layouts thus sometime some contents aren't shown or overflows out of the window. But resizing the window would fix this issue. Soon there will be some updates fixing this sort of layout related problems
|
There will be some glitches, lags & stuck motions because of the library Spotube is currently using under the hood. It has some issues with layouts thus sometimes some contents aren't shown or overflows out of the window. But resizing the window would fix this issue. Soon there will be some updates fixing this sort of layout related problems
|
||||||
|
|
||||||
## TODO:
|
## TODO:
|
||||||
- [ ] Compile, Debug & Build for **MacOS**
|
- [ ] Compile, Debug & Build for **MacOS**
|
||||||
@ -158,8 +157,7 @@ There will be some glitches, lags & stuck motions because of the library Spotube
|
|||||||
|
|
||||||
## Things that don't work
|
## Things that don't work
|
||||||
- Shows & Podcasts aren't supported as it'd require premium anyway
|
- Shows & Podcasts aren't supported as it'd require premium anyway
|
||||||
- Beautiful UI (you missed it, see the title😂^)
|
- Beautiful UI (you missed it, see the title😂👆)
|
||||||
- Images aren't added to ensure the lowest resource usage
|
|
||||||
- OS Media Controls
|
- OS Media Controls
|
||||||
|
|
||||||
#### Social handlers
|
#### Social handlers
|
||||||
|
1
assets/download-solid.svg
Normal file
1
assets/download-solid.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="download" class="svg-inline--fa fa-download fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"></path></svg>
|
After Width: | Height: | Size: 678 B |
BIN
assets/spotube-screenshot.png
Normal file
BIN
assets/spotube-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 380 KiB |
9273
package-lock.json
generated
9273
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -41,7 +41,8 @@
|
|||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"scrape-yt": "^1.4.7",
|
"scrape-yt": "^1.4.7",
|
||||||
"spotify-web-api-node": "^5.0.2",
|
"spotify-web-api-node": "^5.0.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2",
|
||||||
|
"ytdl-core": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.13.10",
|
"@babel/core": "^7.13.10",
|
||||||
|
@ -6,12 +6,13 @@ import { shuffleArray } from "../helpers/shuffleArray";
|
|||||||
import NodeMpv from "node-mpv";
|
import NodeMpv from "node-mpv";
|
||||||
import { getYoutubeTrack, YoutubeTrack } from "../helpers/getYoutubeTrack";
|
import { getYoutubeTrack, YoutubeTrack } from "../helpers/getYoutubeTrack";
|
||||||
import PlayerProgressBar from "./PlayerProgressBar";
|
import PlayerProgressBar from "./PlayerProgressBar";
|
||||||
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart, musicNode } from "../icons";
|
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular, heart, musicNode, download } from "../icons";
|
||||||
import IconButton from "./shared/IconButton";
|
import IconButton from "./shared/IconButton";
|
||||||
import showError from "../helpers/showError";
|
import showError from "../helpers/showError";
|
||||||
import useTrackReaction from "../hooks/useTrackReaction";
|
import useTrackReaction from "../hooks/useTrackReaction";
|
||||||
import ManualLyricDialog from "./ManualLyricDialog";
|
import ManualLyricDialog from "./ManualLyricDialog";
|
||||||
import { LocalStorageKeys } from "../conf";
|
import { LocalStorageKeys } from "../conf";
|
||||||
|
import useDownloadQueue from "../hooks/useDownloadQueue";
|
||||||
|
|
||||||
export const audioPlayer = new NodeMpv(
|
export const audioPlayer = new NodeMpv(
|
||||||
{
|
{
|
||||||
@ -27,7 +28,9 @@ export const audioPlayer = new NodeMpv(
|
|||||||
function Player(): ReactElement {
|
function Player(): ReactElement {
|
||||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
||||||
const { reactToTrack, isFavorite } = useTrackReaction();
|
const { reactToTrack, isFavorite } = useTrackReaction();
|
||||||
|
|
||||||
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
const cachedVolume = localStorage.getItem(LocalStorageKeys.volume);
|
||||||
|
|
||||||
const [isPaused, setIsPaused] = useState(true);
|
const [isPaused, setIsPaused] = useState(true);
|
||||||
const [volume, setVolume] = useState<number>(() => (cachedVolume ? parseFloat(cachedVolume) : 55));
|
const [volume, setVolume] = useState<number>(() => (cachedVolume ? parseFloat(cachedVolume) : 55));
|
||||||
const [totalDuration, setTotalDuration] = useState<number>(0);
|
const [totalDuration, setTotalDuration] = useState<number>(0);
|
||||||
@ -36,6 +39,8 @@ function Player(): ReactElement {
|
|||||||
const [isStopped, setIsStopped] = useState<boolean>(false);
|
const [isStopped, setIsStopped] = useState<boolean>(false);
|
||||||
const [openLyrics, setOpenLyrics] = useState<boolean>(false);
|
const [openLyrics, setOpenLyrics] = useState<boolean>(false);
|
||||||
const [currentYtTrack, setCurrentYtTrack] = useState<YoutubeTrack>();
|
const [currentYtTrack, setCurrentYtTrack] = useState<YoutubeTrack>();
|
||||||
|
const { addToQueue, isActiveDownloading, isFinishedDownloading } = useDownloadQueue();
|
||||||
|
|
||||||
const playlistTracksIds = currentPlaylist?.tracks.map((t) => t.track.id);
|
const playlistTracksIds = currentPlaylist?.tracks.map((t) => t.track.id);
|
||||||
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
const volumeHandler = useEventHandler<QAbstractSliderSignals>(
|
||||||
{
|
{
|
||||||
@ -230,6 +235,16 @@ function Player(): ReactElement {
|
|||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn width={2}>
|
<GridColumn width={2}>
|
||||||
<BoxView>
|
<BoxView>
|
||||||
|
<IconButton
|
||||||
|
style={isActiveDownloading() && !isFinishedDownloading() ? "background-color: green;" : ""}
|
||||||
|
enabled={!!currentYtTrack}
|
||||||
|
icon={new QIcon(download)}
|
||||||
|
on={{
|
||||||
|
clicked() {
|
||||||
|
currentYtTrack && addToQueue(currentYtTrack);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
on={{
|
on={{
|
||||||
clicked() {
|
clicked() {
|
||||||
|
59
src/hooks/useDownloadQueue.ts
Normal file
59
src/hooks/useDownloadQueue.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import ytdl from "ytdl-core";
|
||||||
|
import fs from "fs";
|
||||||
|
import { YoutubeTrack } from "../helpers/getYoutubeTrack";
|
||||||
|
import { join } from "path";
|
||||||
|
import os from "os";
|
||||||
|
import playerContext from "../context/playerContext";
|
||||||
|
import showError from "../helpers/showError";
|
||||||
|
|
||||||
|
function useDownloadQueue() {
|
||||||
|
const [downloadQueue, setDownloadQueue] = useState<YoutubeTrack[]>([]);
|
||||||
|
const [completedQueue, setCompletedQueue] = useState<YoutubeTrack[]>([]);
|
||||||
|
const { currentTrack } = useContext(playerContext);
|
||||||
|
|
||||||
|
function addToQueue(obj: YoutubeTrack) {
|
||||||
|
setDownloadQueue([...downloadQueue, obj]);
|
||||||
|
}
|
||||||
|
const completedTrackIds = completedQueue.map((x) => x.id);
|
||||||
|
const downloadingTrackIds = downloadQueue.map((x) => x.id);
|
||||||
|
|
||||||
|
function isActiveDownloading() {
|
||||||
|
return downloadingTrackIds.includes(currentTrack?.id ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFinishedDownloading() {
|
||||||
|
return completedTrackIds.includes(currentTrack?.id ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
downloadQueue.forEach(async (el) => {
|
||||||
|
if (!completedTrackIds.includes(el.id)) {
|
||||||
|
ytdl(el.youtube_uri, {
|
||||||
|
filter: "audioonly",
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
fs.createWriteStream(
|
||||||
|
join(
|
||||||
|
os.homedir(),
|
||||||
|
"Music",
|
||||||
|
`${el.name} - ${el.artists
|
||||||
|
.map((x) => x.name)
|
||||||
|
.join(", ")
|
||||||
|
.trim()}.mp3`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.on("error", (err) => {
|
||||||
|
showError(err, `[failed to download ${el.name}]: `);
|
||||||
|
})
|
||||||
|
.on("finish", () => {
|
||||||
|
setCompletedQueue([...completedQueue, el]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [downloadQueue]);
|
||||||
|
return { addToQueue, isFinishedDownloading, isActiveDownloading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDownloadQueue;
|
@ -12,6 +12,7 @@ import _loadingSpinner from "../assets/loading-spinner.gif";
|
|||||||
import _settingsCog from "../assets/setting-cog.svg"
|
import _settingsCog from "../assets/setting-cog.svg"
|
||||||
import _times from "../assets/times-solid.svg"
|
import _times from "../assets/times-solid.svg"
|
||||||
import _musicNode from "../assets/music-solid.svg"
|
import _musicNode from "../assets/music-solid.svg"
|
||||||
|
import _download from "../assets/download-solid.svg"
|
||||||
|
|
||||||
export const play = _play;
|
export const play = _play;
|
||||||
export const pause = _pause;
|
export const pause = _pause;
|
||||||
@ -27,3 +28,4 @@ export const loadingSpinner = _loadingSpinner;
|
|||||||
export const settingsCog = _settingsCog;
|
export const settingsCog = _settingsCog;
|
||||||
export const times = _times;
|
export const times = _times;
|
||||||
export const musicNode = _musicNode;
|
export const musicNode = _musicNode;
|
||||||
|
export const download = _download;
|
Loading…
Reference in New Issue
Block a user