From cf1b98cce35c50b232adfdbae081800b99657d9d Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:12:50 -0400 Subject: [PATCH 01/26] Fix #105 renamed src to zspotify --- {src => zspotify}/__main__.py | 0 {src => zspotify}/album.py | 0 {src => zspotify}/app.py | 0 {src => zspotify}/const.py | 0 {src => zspotify}/playlist.py | 0 {src => zspotify}/podcast.py | 0 {src => zspotify}/track.py | 0 {src => zspotify}/utils.py | 0 {src => zspotify}/zspotify.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {src => zspotify}/__main__.py (100%) rename {src => zspotify}/album.py (100%) rename {src => zspotify}/app.py (100%) rename {src => zspotify}/const.py (100%) rename {src => zspotify}/playlist.py (100%) rename {src => zspotify}/podcast.py (100%) rename {src => zspotify}/track.py (100%) rename {src => zspotify}/utils.py (100%) rename {src => zspotify}/zspotify.py (100%) diff --git a/src/__main__.py b/zspotify/__main__.py similarity index 100% rename from src/__main__.py rename to zspotify/__main__.py diff --git a/src/album.py b/zspotify/album.py similarity index 100% rename from src/album.py rename to zspotify/album.py diff --git a/src/app.py b/zspotify/app.py similarity index 100% rename from src/app.py rename to zspotify/app.py diff --git a/src/const.py b/zspotify/const.py similarity index 100% rename from src/const.py rename to zspotify/const.py diff --git a/src/playlist.py b/zspotify/playlist.py similarity index 100% rename from src/playlist.py rename to zspotify/playlist.py diff --git a/src/podcast.py b/zspotify/podcast.py similarity index 100% rename from src/podcast.py rename to zspotify/podcast.py diff --git a/src/track.py b/zspotify/track.py similarity index 100% rename from src/track.py rename to zspotify/track.py diff --git a/src/utils.py b/zspotify/utils.py similarity index 100% rename from src/utils.py rename to zspotify/utils.py diff --git a/src/zspotify.py b/zspotify/zspotify.py similarity index 100% rename from src/zspotify.py rename to zspotify/zspotify.py From cf49ab8c5250dc82f57f142a12a6fd153b8c8be7 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:17:11 -0400 Subject: [PATCH 02/26] Updated python usage to reflect app folder rename --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 80cfde88..1fed8122 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python src Loads search prompt to find then download a specific track, album or playlist - python src Downloads the track, album, playlist or podcast episode specified as a command line argument - python src Downloads all albums by specified artist + python zspotify Loads search prompt to find then download a specific track, album or playlist + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument + python zspotify Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From 25740d9589e95ca138962fa46e38f95d587e11d3 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:26 -0400 Subject: [PATCH 03/26] Revert "Merge pull request #107 from jaredrossberg/docker" This reverts commit 898e20dc6dbfeca3c52cfc157f000af84c281a06, reversing changes made to 938b64775383586e86c9a357097292487cbccc6d. --- Dockerfile | 2 +- README.md | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index dcba36ca..ceb88cf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /install /usr/local COPY src /app COPY zs_config.json / WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] +ENTRYPOINT ["/usr/local/bin/python", "app.py"] diff --git a/README.md b/README.md index 1fed8122..425e1cbf 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,6 @@ Options that can be configured in zs_config.json: ANTI_BAN_WAIT_TIME Change this setting if the time waited between bulk downloads is too high or low OVERRIDE_AUTO_WAIT Change this to true if you want to completely disable the wait between songs for faster downloads with the risk of instability ``` - -``` -Docker usage: - docker build -t zspotify . Builds the docker image from the Dockerfile - docker run --name zspotify zspotify Creates and runs a container from the image -``` - ### Will my account get banned if I use this tool? Currently no user has reported their account getting banned after using ZSpotify. This isn't to say _you_ won't get banned as it is technically against Spotify's TOS. From 81262567f82bb8a5e9a28817dfd16edfcb577339 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:33 -0400 Subject: [PATCH 04/26] Revert "Fix #105 renamed src to zspotify" This reverts commit cf1b98cce35c50b232adfdbae081800b99657d9d. --- {zspotify => src}/__main__.py | 0 {zspotify => src}/album.py | 0 {zspotify => src}/app.py | 0 {zspotify => src}/const.py | 0 {zspotify => src}/playlist.py | 0 {zspotify => src}/podcast.py | 0 {zspotify => src}/track.py | 0 {zspotify => src}/utils.py | 0 {zspotify => src}/zspotify.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {zspotify => src}/__main__.py (100%) rename {zspotify => src}/album.py (100%) rename {zspotify => src}/app.py (100%) rename {zspotify => src}/const.py (100%) rename {zspotify => src}/playlist.py (100%) rename {zspotify => src}/podcast.py (100%) rename {zspotify => src}/track.py (100%) rename {zspotify => src}/utils.py (100%) rename {zspotify => src}/zspotify.py (100%) diff --git a/zspotify/__main__.py b/src/__main__.py similarity index 100% rename from zspotify/__main__.py rename to src/__main__.py diff --git a/zspotify/album.py b/src/album.py similarity index 100% rename from zspotify/album.py rename to src/album.py diff --git a/zspotify/app.py b/src/app.py similarity index 100% rename from zspotify/app.py rename to src/app.py diff --git a/zspotify/const.py b/src/const.py similarity index 100% rename from zspotify/const.py rename to src/const.py diff --git a/zspotify/playlist.py b/src/playlist.py similarity index 100% rename from zspotify/playlist.py rename to src/playlist.py diff --git a/zspotify/podcast.py b/src/podcast.py similarity index 100% rename from zspotify/podcast.py rename to src/podcast.py diff --git a/zspotify/track.py b/src/track.py similarity index 100% rename from zspotify/track.py rename to src/track.py diff --git a/zspotify/utils.py b/src/utils.py similarity index 100% rename from zspotify/utils.py rename to src/utils.py diff --git a/zspotify/zspotify.py b/src/zspotify.py similarity index 100% rename from zspotify/zspotify.py rename to src/zspotify.py From c131a8a87562553d7ec82efd4c3d2a21037c7eee Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:37 -0400 Subject: [PATCH 05/26] Revert "Updated python usage to reflect app folder rename" This reverts commit cf49ab8c5250dc82f57f142a12a6fd153b8c8be7. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 425e1cbf..ac87b944 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python zspotify Loads search prompt to find then download a specific track, album or playlist - python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument - python zspotify Downloads all albums by specified artist + python src Loads search prompt to find then download a specific track, album or playlist + python src Downloads the track, album, playlist or podcast episode specified as a command line argument + python src Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From b5256acb897648a5e4015fd15dd94513a307cf7d Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:12:50 -0400 Subject: [PATCH 06/26] Fix #105 renamed src to zspotify --- {src => zspotify}/__main__.py | 0 {src => zspotify}/album.py | 0 {src => zspotify}/app.py | 0 {src => zspotify}/const.py | 0 {src => zspotify}/playlist.py | 0 {src => zspotify}/podcast.py | 0 {src => zspotify}/track.py | 0 {src => zspotify}/utils.py | 0 {src => zspotify}/zspotify.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {src => zspotify}/__main__.py (100%) rename {src => zspotify}/album.py (100%) rename {src => zspotify}/app.py (100%) rename {src => zspotify}/const.py (100%) rename {src => zspotify}/playlist.py (100%) rename {src => zspotify}/podcast.py (100%) rename {src => zspotify}/track.py (100%) rename {src => zspotify}/utils.py (100%) rename {src => zspotify}/zspotify.py (100%) diff --git a/src/__main__.py b/zspotify/__main__.py similarity index 100% rename from src/__main__.py rename to zspotify/__main__.py diff --git a/src/album.py b/zspotify/album.py similarity index 100% rename from src/album.py rename to zspotify/album.py diff --git a/src/app.py b/zspotify/app.py similarity index 100% rename from src/app.py rename to zspotify/app.py diff --git a/src/const.py b/zspotify/const.py similarity index 100% rename from src/const.py rename to zspotify/const.py diff --git a/src/playlist.py b/zspotify/playlist.py similarity index 100% rename from src/playlist.py rename to zspotify/playlist.py diff --git a/src/podcast.py b/zspotify/podcast.py similarity index 100% rename from src/podcast.py rename to zspotify/podcast.py diff --git a/src/track.py b/zspotify/track.py similarity index 100% rename from src/track.py rename to zspotify/track.py diff --git a/src/utils.py b/zspotify/utils.py similarity index 100% rename from src/utils.py rename to zspotify/utils.py diff --git a/src/zspotify.py b/zspotify/zspotify.py similarity index 100% rename from src/zspotify.py rename to zspotify/zspotify.py From f60ede3c45ae7ba9a39678cca361e13750b18d6d Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:17:11 -0400 Subject: [PATCH 07/26] Updated python usage to reflect app folder rename --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e6ea1f9..8b5da667 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python src Loads search prompt to find then download a specific track, album or playlist - python src Downloads the track, album, playlist or podcast episode specified as a command line argument - python src Downloads all albums by specified artist + python zspotify Loads search prompt to find then download a specific track, album or playlist + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument + python zspotify Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From 5966bc00b7de5776af6755f6d777d01a353d6e94 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:26 -0400 Subject: [PATCH 08/26] Revert "Merge pull request #107 from jaredrossberg/docker" This reverts commit 898e20dc6dbfeca3c52cfc157f000af84c281a06, reversing changes made to 938b64775383586e86c9a357097292487cbccc6d. --- Dockerfile | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index dcba36ca..ceb88cf2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /install /usr/local COPY src /app COPY zs_config.json / WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] +ENTRYPOINT ["/usr/local/bin/python", "app.py"] diff --git a/README.md b/README.md index 8b5da667..548a9af5 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Options that can be configured in zs_config.json: ### Docker Usage -``` +``` Build the docker image from the Dockerfile: docker build -t zspotify . Create and run a container from the image: From 868c7f6c188b296453f7e34694ed567703db5317 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:33 -0400 Subject: [PATCH 09/26] Revert "Fix #105 renamed src to zspotify" This reverts commit cf1b98cce35c50b232adfdbae081800b99657d9d. --- {zspotify => src}/__main__.py | 0 {zspotify => src}/album.py | 0 {zspotify => src}/app.py | 0 {zspotify => src}/const.py | 0 {zspotify => src}/playlist.py | 0 {zspotify => src}/podcast.py | 0 {zspotify => src}/track.py | 0 {zspotify => src}/utils.py | 0 {zspotify => src}/zspotify.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {zspotify => src}/__main__.py (100%) rename {zspotify => src}/album.py (100%) rename {zspotify => src}/app.py (100%) rename {zspotify => src}/const.py (100%) rename {zspotify => src}/playlist.py (100%) rename {zspotify => src}/podcast.py (100%) rename {zspotify => src}/track.py (100%) rename {zspotify => src}/utils.py (100%) rename {zspotify => src}/zspotify.py (100%) diff --git a/zspotify/__main__.py b/src/__main__.py similarity index 100% rename from zspotify/__main__.py rename to src/__main__.py diff --git a/zspotify/album.py b/src/album.py similarity index 100% rename from zspotify/album.py rename to src/album.py diff --git a/zspotify/app.py b/src/app.py similarity index 100% rename from zspotify/app.py rename to src/app.py diff --git a/zspotify/const.py b/src/const.py similarity index 100% rename from zspotify/const.py rename to src/const.py diff --git a/zspotify/playlist.py b/src/playlist.py similarity index 100% rename from zspotify/playlist.py rename to src/playlist.py diff --git a/zspotify/podcast.py b/src/podcast.py similarity index 100% rename from zspotify/podcast.py rename to src/podcast.py diff --git a/zspotify/track.py b/src/track.py similarity index 100% rename from zspotify/track.py rename to src/track.py diff --git a/zspotify/utils.py b/src/utils.py similarity index 100% rename from zspotify/utils.py rename to src/utils.py diff --git a/zspotify/zspotify.py b/src/zspotify.py similarity index 100% rename from zspotify/zspotify.py rename to src/zspotify.py From 68dffda950eeae3d6db203238985c0f94038b592 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:29:37 -0400 Subject: [PATCH 10/26] Revert "Updated python usage to reflect app folder rename" This reverts commit cf49ab8c5250dc82f57f142a12a6fd153b8c8be7. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 548a9af5..f6ec0be1 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python zspotify Loads search prompt to find then download a specific track, album or playlist - python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument - python zspotify Downloads all albums by specified artist + python src Loads search prompt to find then download a specific track, album or playlist + python src Downloads the track, album, playlist or podcast episode specified as a command line argument + python src Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account From b9d3f37e423ff9cdaae4e5ba17c9588d84d468f5 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:43:32 -0400 Subject: [PATCH 11/26] Fixed previous errors trying to rebase --- Dockerfile | 2 +- README.md | 6 +++--- {src => zspotify}/__main__.py | 0 {src => zspotify}/album.py | 0 {src => zspotify}/app.py | 2 +- {src => zspotify}/const.py | 0 {src => zspotify}/playlist.py | 0 {src => zspotify}/podcast.py | 0 {src => zspotify}/track.py | 0 {src => zspotify}/utils.py | 0 {src => zspotify}/zspotify.py | 0 11 files changed, 5 insertions(+), 5 deletions(-) rename {src => zspotify}/__main__.py (100%) rename {src => zspotify}/album.py (100%) rename {src => zspotify}/app.py (99%) rename {src => zspotify}/const.py (100%) rename {src => zspotify}/playlist.py (100%) rename {src => zspotify}/podcast.py (100%) rename {src => zspotify}/track.py (100%) rename {src => zspotify}/utils.py (100%) rename {src => zspotify}/zspotify.py (100%) diff --git a/Dockerfile b/Dockerfile index ceb88cf2..dcba36ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /install /usr/local COPY src /app COPY zs_config.json / WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "app.py"] +ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] diff --git a/README.md b/README.md index f6ec0be1..548a9af5 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` Command line usage: - python src Loads search prompt to find then download a specific track, album or playlist - python src Downloads the track, album, playlist or podcast episode specified as a command line argument - python src Downloads all albums by specified artist + python zspotify Loads search prompt to find then download a specific track, album or playlist + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument + python zspotify Downloads all albums by specified artist Extra command line options: -p, --playlist Downloads a saved playlist from your account diff --git a/src/__main__.py b/zspotify/__main__.py similarity index 100% rename from src/__main__.py rename to zspotify/__main__.py diff --git a/src/album.py b/zspotify/album.py similarity index 100% rename from src/album.py rename to zspotify/album.py diff --git a/src/app.py b/zspotify/app.py similarity index 99% rename from src/app.py rename to zspotify/app.py index 5684b994..a2433a57 100644 --- a/src/app.py +++ b/zspotify/app.py @@ -33,7 +33,7 @@ def client() -> None: elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': for song in get_saved_tracks(): if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXISTS ON SPOTIFY ANYMORE ###') + print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') else: download_track(song[TRACK][ID], 'Liked Songs/') print('\n') diff --git a/src/const.py b/zspotify/const.py similarity index 100% rename from src/const.py rename to zspotify/const.py diff --git a/src/playlist.py b/zspotify/playlist.py similarity index 100% rename from src/playlist.py rename to zspotify/playlist.py diff --git a/src/podcast.py b/zspotify/podcast.py similarity index 100% rename from src/podcast.py rename to zspotify/podcast.py diff --git a/src/track.py b/zspotify/track.py similarity index 100% rename from src/track.py rename to zspotify/track.py diff --git a/src/utils.py b/zspotify/utils.py similarity index 100% rename from src/utils.py rename to zspotify/utils.py diff --git a/src/zspotify.py b/zspotify/zspotify.py similarity index 100% rename from src/zspotify.py rename to zspotify/zspotify.py From 693cc068c42bdccacf94018ac1dd81de14a85e89 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Tue, 26 Oct 2021 01:39:38 -0400 Subject: [PATCH 12/26] Added argparser and updated Readme for cli usage --- README.md | 8 ++--- zspotify/__main__.py | 31 ++++++++++++++++- zspotify/app.py | 79 +++++++++++++++++++++++--------------------- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 060509c2..083238c1 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,14 @@ Python packages: ### Command line usage: ``` -Basic usage: - python zspotify Loads search prompt to find then download a specific track, album or playlist - python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument - python zspotify Downloads all albums by specified artist +Basic command line usage: + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Extra command line options: -p, --playlist Downloads a saved playlist from your account -ls, --liked-songs Downloads all the liked songs from your account + -s, --search Loads search prompt to find then download a specific track, album or playlist + -ns, --no-splash Suppress the splash screen when loading. Options that can be configured in zs_config.json: ROOT_PATH Change this path if you don't like the default directory where ZSpotify saves the music diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 50dd3890..9832af86 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -1,4 +1,33 @@ +import argparse + from app import client + + if __name__ == '__main__': - client() + parser = argparse.ArgumentParser(prog='zspotify', description='A Spotify downloader needing only a python interpreter and ffmpeg.') + parser.add_argument('-ns', '--no-splash', + action='store_true', + help='Suppress the splash screen when loading.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('url', + type=str, + default='', + nargs='?', + help='Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded.') + group.add_argument('-ls', '--liked-songs', + dest='liked_songs', + action='store_true', + help='Downloads all the liked songs from your account.') + group.add_argument('-p', '--playlist', + action='store_true', + help='Downloads a saved playlist from your account.') + group.add_argument('-s', '--search', + dest='search_spotify', + action='store_true', + help='Loads search prompt to find then download a specific track, album or playlist') + + parser.set_defaults(func=client) + + args = parser.parse_args() + args.func(args) diff --git a/zspotify/app.py b/zspotify/app.py index 3ee926b1..ad184df9 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -1,5 +1,3 @@ -import sys - from librespot.audio.decoders import AudioQuality from tabulate import tabulate @@ -15,53 +13,58 @@ from zspotify import ZSpotify SEARCH_URL = 'https://api.spotify.com/v1/search' -def client() -> None: +def client(args) -> None: """ Connects to spotify to perform query's and get songs to download """ ZSpotify() - splash() + + if not args.no_splash: + splash() if ZSpotify.check_premium(): - print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') + if not args.no_splash: + print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH else: - print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') + if not args.no_splash: + print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH - if len(sys.argv) > 1: - if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': - download_from_user_playlist() - elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': - for song in get_saved_tracks(): - if not song[TRACK][NAME]: - print( - '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') - else: - download_track(song[TRACK][ID], 'Liked Songs/') + if args.url: + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + args.url) + + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], + sanitize_data(name) + '/') print('\n') - else: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( - sys.argv[1]) + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], - sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) + if args.playlist: + download_from_user_playlist() - else: + if args.liked_songs: + for song in get_saved_tracks(): + if not song[TRACK][NAME]: + print( + '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + else: + download_track(song[TRACK][ID], 'Liked Songs/') + print('\n') + + if args.search_spotify: search_text = '' while len(search_text) == 0: search_text = input('Enter search or URL: ') From ab35683045a5b1b9e6689d0b0c2a56943895de7b Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Tue, 26 Oct 2021 12:37:10 -0400 Subject: [PATCH 13/26] Amended help text --- zspotify/__main__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 9832af86..875b7676 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -5,7 +5,8 @@ from app import client if __name__ == '__main__': - parser = argparse.ArgumentParser(prog='zspotify', description='A Spotify downloader needing only a python interpreter and ffmpeg.') + parser = argparse.ArgumentParser(prog='zspotify', + description='A Spotify downloader needing only a python interpreter and ffmpeg.') parser.add_argument('-ns', '--no-splash', action='store_true', help='Suppress the splash screen when loading.') @@ -14,7 +15,7 @@ if __name__ == '__main__': type=str, default='', nargs='?', - help='Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded.') + help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url.') group.add_argument('-ls', '--liked-songs', dest='liked_songs', action='store_true', From 01c8dd49fdf658e1f74e87a5e4364e35c63e6361 Mon Sep 17 00:00:00 2001 From: Girish Kotra Date: Wed, 27 Oct 2021 13:53:54 +0530 Subject: [PATCH 14/26] revised playlist id parsing logic in playlist.download_from_user_playlist --- zspotify/playlist.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 9a1304c2..06552b43 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -71,17 +71,14 @@ def download_from_user_playlist(): print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s') print('> For example, typing 10 to get one playlist or 10-20 to get\nevery playlist from 10-20 (inclusive)\n') - playlist_choices = input('ID(s): ').split('-') + playlist_choices = map(int, input('ID(s): ').split('-')) - if len(playlist_choices) == 1: - download_playlist(playlists, playlist_choices[0]) - else: - start = int(playlist_choices[0]) - end = int(playlist_choices[1]) + 1 + start = next(playlist_choices) - 1 + end = next(playlist_choices, start + 1) - print(f'Downloading from {start} to {end}...') + for playlist_number in range(start, end): + playlist = playlists[playlist_number] + print(f'Downloading {playlist[NAME].strip()}') + download_playlist(playlist) - for playlist_number in range(start, end): - download_playlist(playlists, playlist_number) - - print('\n**All playlists have been downloaded**\n') + print('\n**All playlists have been downloaded**\n') From 132c235bdb6b5ab54426beb05ff082feab8d952a Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 02:14:18 -0400 Subject: [PATCH 15/26] remade changes after merging with main --- README.md | 8 ++--- zspotify/__main__.py | 32 ++++++++++++++++++- zspotify/app.py | 76 +++++++++++++++++++++++--------------------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 548a9af5..b83d9533 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ Python packages: \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. ``` -Command line usage: - python zspotify Loads search prompt to find then download a specific track, album or playlist - python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument - python zspotify Downloads all albums by specified artist +Basic command line usage: + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Extra command line options: -p, --playlist Downloads a saved playlist from your account -ls, --liked-songs Downloads all the liked songs from your account + -s, --search Loads search prompt to find then download a specific track, album or playlist + -ns, --no-splash Suppress the splash screen when loading. Options that can be configured in zs_config.json: ROOT_PATH Change this path if you don't like the default directory where ZSpotify saves the music diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 50dd3890..875b7676 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -1,4 +1,34 @@ +import argparse + from app import client + + if __name__ == '__main__': - client() + parser = argparse.ArgumentParser(prog='zspotify', + description='A Spotify downloader needing only a python interpreter and ffmpeg.') + parser.add_argument('-ns', '--no-splash', + action='store_true', + help='Suppress the splash screen when loading.') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('url', + type=str, + default='', + nargs='?', + help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url.') + group.add_argument('-ls', '--liked-songs', + dest='liked_songs', + action='store_true', + help='Downloads all the liked songs from your account.') + group.add_argument('-p', '--playlist', + action='store_true', + help='Downloads a saved playlist from your account.') + group.add_argument('-s', '--search', + dest='search_spotify', + action='store_true', + help='Loads search prompt to find then download a specific track, album or playlist') + + parser.set_defaults(func=client) + + args = parser.parse_args() + args.func(args) diff --git a/zspotify/app.py b/zspotify/app.py index a2433a57..0c029d03 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -1,5 +1,3 @@ -import sys - from librespot.audio.decoders import AudioQuality from tabulate import tabulate @@ -15,51 +13,56 @@ from zspotify import ZSpotify SEARCH_URL = 'https://api.spotify.com/v1/search' -def client() -> None: +def client(args) -> None: """ Connects to spotify to perform query's and get songs to download """ ZSpotify() - splash() + + if not args.no_splash: + splash() if ZSpotify.check_premium(): - print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') + if not args.no_splash: + print('[ DETECTED PREMIUM ACCOUNT - USING VERY_HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.VERY_HIGH else: - print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') + if not args.no_splash: + print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH - if len(sys.argv) > 1: - if sys.argv[1] == '-p' or sys.argv[1] == '--playlist': - download_from_user_playlist() - elif sys.argv[1] == '-ls' or sys.argv[1] == '--liked-songs': - for song in get_saved_tracks(): - if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') - else: - download_track(song[TRACK][ID], 'Liked Songs/') + if args.url: + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(args.url) + + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], + sanitize_data(name) + '/') print('\n') - else: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(sys.argv[1]) + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], - sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) + if args.playlist: + download_from_user_playlist() - else: + if args.liked_songs: + for song in get_saved_tracks(): + if not song[TRACK][NAME]: + print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + else: + download_track(song[TRACK][ID], 'Liked Songs/') + print('\n') + + if args.search_spotify: search_text = '' while len(search_text) == 0: search_text = input('Enter search or URL: ') @@ -261,4 +264,3 @@ def search(search_term): download_artist_albums(dic[ID]) else: download_playlist(dic) - From 95f0dc8edfa3166e58c4287be2fa9d666f40e565 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 02:17:38 -0400 Subject: [PATCH 16/26] Update README.md --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b83d9533..979bafc0 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,15 @@ Create and run a container from the image: ``` ### Will my account get banned if I use this tool? -Currently no user has reported their account getting banned after using ZSpotify. -This isn't to say _you_ won't get banned as it is technically against Spotify's TOS. + +~~Currently no user has reported their account getting banned after using ZSpotify.~~ + +**There have been 2-3 reports from users who received account bans from Spotify for using this tool**. + +We recommend using ZSpotify with a burner account. +Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus not appearing suspicious to Spotify. +This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account. + **Use ZSpotify at your own risk**, the developers of ZSpotify are not responsible if your account gets banned. ### What do I do if I see "Your session has been terminated"? From efe884b58a8d8a1c32b55339d85a9d4f4be97859 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 02:21:08 -0400 Subject: [PATCH 17/26] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 83de5a5b..40838a46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ COPY --from=builder /install /usr/local COPY zspotify /app COPY *zs_config.json / WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "app.py"] +ENTRYPOINT ["/usr/local/bin/python", "__main__.py"] From 9a93544cb4fdada72183c6f71d08caa499e978f8 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 02:21:40 -0400 Subject: [PATCH 18/26] Delete Dockerfile --- Dockerfile | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 83de5a5b..00000000 --- a/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.9-alpine as base - -RUN apk --update add git ffmpeg - -FROM base as builder -RUN mkdir /install -WORKDIR /install -COPY requirements.txt /requirements.txt -RUN apk add gcc libc-dev zlib zlib-dev jpeg-dev \ - && pip install --prefix="/install" -r /requirements.txt - - -FROM base - -COPY --from=builder /install /usr/local -COPY zspotify /app -COPY *zs_config.json / -WORKDIR /app -ENTRYPOINT ["/usr/local/bin/python", "app.py"] From 7412abcee5b61d9dcd7f3635ff5c27b4ccf540af Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 02:25:57 -0400 Subject: [PATCH 19/26] Revert "Merge branch 'main' into argument-parsing" This reverts commit e7b3f5c4bdf36b23fccc7964eec37b905eebb6d4, reversing changes made to 95f0dc8edfa3166e58c4287be2fa9d666f40e565. --- .github/workflows/pylint.yml | 4 +- CHANGELOG.md | 6 --- README.md | 3 +- requirements.txt | 8 ++-- zspotify/album.py | 2 +- zspotify/const.py | 29 +------------ zspotify/playlist.py | 4 +- zspotify/podcast.py | 44 ++++++------------- zspotify/track.py | 82 ++++++++++-------------------------- 9 files changed, 45 insertions(+), 137 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c742569a..0b88fe7a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -16,8 +16,8 @@ jobs: - name: Setup Pylint run: | python -m pip install --upgrade pip - pip install pylint pylint-exit + pip install pylint pip install -r requirements.txt - name: Run Pylint run: | - pylint $(git ls-files '*.py') || pylint-exit $? + pylint $(git ls-files '*.py') diff --git a/CHANGELOG.md b/CHANGELOG.md index c7001d48..9f93e54e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,4 @@ ## **Changelog:** -**v2.4 (27 Oct 20212):** -- Added realtime downloading support to avoid account suspensions. -- Fix for downloading by artist. -- Replace audio conversion method for better quality. -- Fix bug when automatically setting audio bitrate. - **v2.3 (25 Oct 2021):** - Moved changelog to seperate file. - Added argument parsing in search function (query results limit and query result types). diff --git a/README.md b/README.md index e1b7dff7..979bafc0 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,7 @@ Create and run a container from the image: **There have been 2-3 reports from users who received account bans from Spotify for using this tool**. -We recommend using ZSpotify with a burner account. - +We recommend using ZSpotify with a burner account. Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus not appearing suspicious to Spotify. This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account. diff --git a/requirements.txt b/requirements.txt index b1877ecf..4f4b877e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ -ffmpy git+https://github.com/kokarare1212/librespot-python music_tag -Pillow -protobuf pydub -tabulate -tqdm \ No newline at end of file +Pillow +tqdm +tabulate \ No newline at end of file diff --git a/zspotify/album.py b/zspotify/album.py index 2070da24..19004d11 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -33,7 +33,7 @@ def get_album_name(album_id): def get_artist_albums(artist_id): """ Returns artist's albums """ - resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums?include_groups=album%2Csingle') + resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums') # Return a list each album's id album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] # Recursive requests to get all albums including singles an EPs diff --git a/zspotify/const.py b/zspotify/const.py index 1ec7594a..3e4d9b0f 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -96,39 +96,14 @@ CHUNK_SIZE = 'CHUNK_SIZE' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' -DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' - -BITRATE = 'BITRATE' - -CODEC_MAP = { - 'aac': 'aac', - 'fdk_aac': 'libfdk_aac', - 'm4a': 'aac', - 'mp3': 'libmp3lame', - 'ogg': 'copy', - 'opus': 'libopus', - 'vorbis': 'copy', -} - -EXT_MAP = { - 'aac': 'm4a', - 'fdk_aac': 'm4a', - 'm4a': 'm4a', - 'mp3': 'mp3', - 'ogg': 'ogg', - 'opus': 'ogg', - 'vorbis': 'ogg', -} - CONFIG_DEFAULT_SETTINGS = { 'ROOT_PATH': '../ZSpotify Music/', 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', 'SKIP_EXISTING_FILES': True, - 'DOWNLOAD_FORMAT': 'ogg', + 'DOWNLOAD_FORMAT': 'mp3', 'FORCE_PREMIUM': False, 'ANTI_BAN_WAIT_TIME': 1, 'OVERRIDE_AUTO_WAIT': False, 'CHUNK_SIZE': 50000, - 'SPLIT_ALBUM_DISCS': False, - 'DOWNLOAD_REAL_TIME': False + 'SPLIT_ALBUM_DISCS': False } diff --git a/zspotify/playlist.py b/zspotify/playlist.py index cf333d1b..9a1304c2 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -52,12 +52,10 @@ def download_playlist(playlist): playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) - enum = 1 for song in p_bar: download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', - prefix=True, prefix_value=str(enum) ,disable_progressbar=True) + disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) - enum += 1 def download_from_user_playlist(): diff --git a/zspotify/podcast.py b/zspotify/podcast.py index 03bc7b35..eea36c1b 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -5,11 +5,11 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId from tqdm import tqdm -from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW, - SKIP_EXISTING_FILES) -from utils import create_download_directory, sanitize_data +from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE +from utils import sanitize_data, create_download_directory, MusicFormat from zspotify import ZSpotify + EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' SHOWS_URL = 'https://api.spotify.com/v1/shows' @@ -50,41 +50,21 @@ def download_episode(episode_id) -> None: episode_id = EpisodeId.from_base62(episode_id) stream = ZSpotify.get_content_stream(episode_id, ZSpotify.DOWNLOAD_QUALITY) - download_directory = os.path.join( - os.path.dirname(__file__), - ZSpotify.get_config(ROOT_PODCAST_PATH), - extra_paths, - ) - download_directory = os.path.realpath(download_directory) + download_directory = os.path.dirname(__file__) + ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths create_download_directory(download_directory) total_size = stream.input_stream.size - - filepath = os.path.join(download_directory, f"{filename}.ogg") - if ( - os.path.isfile(filepath) - and os.path.getsize(filepath) == total_size - and ZSpotify.get_config(SKIP_EXISTING_FILES) - ): - print( - "\n### SKIPPING:", - podcast_name, - "-", - episode_name, - "(EPISODE ALREADY EXISTS) ###", - ) - return - - with open(filepath, 'wb') as file, tqdm( - desc=filename, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024 + with open(download_directory + filename + MusicFormat.OGG.value, + 'wb') as file, tqdm( + desc=filename, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024 ) as bar: for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): bar.update(file.write( stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) # convert_audio_format(ROOT_PODCAST_PATH + - # extra_paths + filename + '.ogg') + # extra_paths + filename + '.ogg') \ No newline at end of file diff --git a/zspotify/track.py b/zspotify/track.py index 57960496..423cce0c 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -4,14 +4,14 @@ from typing import Any, Tuple, List from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId -from ffmpy import FFmpeg from pydub import AudioSegment from tqdm import tqdm from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \ - SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, BITRATE, CODEC_MAP, EXT_MAP, DOWNLOAD_REAL_TIME -from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory + SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT +from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory, \ + MusicFormat from zspotify import ZSpotify @@ -72,7 +72,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', ) else f'{prefix_value} - {song_name}' filename = os.path.join( - download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT))}') + download_directory, f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') except Exception as e: print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') @@ -81,11 +81,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', try: if not is_playable: print('\n### SKIPPING:', song_name, - '(SONG IS UNAVAILABLE) ###') + '(SONG IS UNAVAILABLE) ###') else: if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): print('\n### SKIPPING:', song_name, - '(SONG ALREADY EXISTS) ###') + '(SONG ALREADY EXISTS) ###') else: if track_id != scraped_song_id: track_id = scraped_song_id @@ -103,21 +103,15 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', unit_divisor=1024, disable=disable_progressbar ) as p_bar: - for chunk in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): - data = stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)) - if data == b'': - break - p_bar.update(file.write(data)) - if ZSpotify.get_config(DOWNLOAD_REAL_TIME): - if chunk == 0: - pause = get_segment_duration(p_bar) - if pause: - time.sleep(pause) + for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): + p_bar.update(file.write( + stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) - convert_audio_format(filename) - set_audio_tags(filename, artists, name, album_name, - release_year, disc_number, track_number) - set_music_thumbnail(filename, image_url) + if ZSpotify.get_config(DOWNLOAD_FORMAT) == 'mp3': + convert_audio_format(filename) + set_audio_tags(filename, artists, name, album_name, + release_year, disc_number, track_number) + set_music_thumbnail(filename, image_url) if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) @@ -129,44 +123,14 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', os.remove(filename) -def get_segment_duration(segment): - """ Returns playback duration of given audio segment """ - sound = AudioSegment( - data = segment, - sample_width = 2, - frame_rate = 44100, - channels = 2 - ) - duration = len(sound) / 5000 - return duration - - def convert_audio_format(filename) -> None: - """ Converts raw audio into playable file """ - temp_filename = f'{os.path.splitext(filename)[0]}.tmp' - os.replace(filename, temp_filename) - - download_format = ZSpotify.get_config(DOWNLOAD_FORMAT) - file_codec = CODEC_MAP.get(download_format, "copy") - if file_codec != 'copy': - bitrate = ZSpotify.get_config(BITRATE) - if not bitrate: - if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: - bitrate = '320k' - else: - bitrate = '160k' + """ Converts raw audio into playable mp3 """ + # print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###') + raw_audio = AudioSegment.from_file(filename, format=MusicFormat.OGG.value, + frame_rate=44100, channels=2, sample_width=2) + if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: + bitrate = '320k' else: - bitrate = None - - output_params = ['-c:a', file_codec] - if bitrate: - output_params += ['-b:a', bitrate] - - ff_m = FFmpeg( - global_options=['-y', '-hide_banner', '-loglevel error'], - inputs={temp_filename: None}, - outputs={filename: output_params} - ) - ff_m.run() - if os.path.exists(temp_filename): - os.remove(temp_filename) + bitrate = '160k' + raw_audio.export(filename, format=ZSpotify.get_config( + DOWNLOAD_FORMAT), bitrate=bitrate) From 8f36a9ab2d5da11c355da2467cb7c5ddbcc9068d Mon Sep 17 00:00:00 2001 From: Thomas Lau Date: Thu, 28 Oct 2021 09:01:40 +0000 Subject: [PATCH 20/26] fix windows invalid folder name --- zspotify/album.py | 6 ++++-- zspotify/utils.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/zspotify/album.py b/zspotify/album.py index 2070da24..8a38b6e5 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -2,7 +2,7 @@ from tqdm import tqdm from const import ITEMS, ARTISTS, NAME, ID from track import download_track -from utils import sanitize_data +from utils import sanitize_data, fix_filename from zspotify import ZSpotify ALBUM_URL = 'https://api.spotify.com/v1/albums' @@ -47,9 +47,11 @@ def get_artist_albums(artist_id): def download_album(album): """ Downloads songs from an album """ artist, album_name = get_album_name(album) + artist_fixed = fix_filename(artist) + album_name_fixed = fix_filename(album_name) tracks = get_album_tracks(album) for n, track in tqdm(enumerate(tracks, start=1), unit_scale=True, unit='Song', total=len(tracks)): - download_track(track[ID], f'{artist}/{album_name}', + download_track(track[ID], f'{artist_fixed}/{album_name_fixed}', prefix=True, prefix_value=str(n), disable_progressbar=True) diff --git a/zspotify/utils.py b/zspotify/utils.py index b8188d95..f6c95840 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -179,3 +179,22 @@ def regex_input_for_urls(search_input) -> Tuple[str, str, str, str, str, str]: artist_id_str = None return track_id_str, album_id_str, playlist_id_str, episode_id_str, show_id_str, artist_id_str + + +def fix_filename(name): + """ + Replace invalid characters on Linux/Windows/MacOS with underscores. + List from https://stackoverflow.com/a/31976060/819417 + Trailing spaces & periods are ignored on Windows. + >>> fix_filename(" COM1 ") + '_ COM1 _' + >>> fix_filename("COM10") + 'COM10' + >>> fix_filename("COM1,") + 'COM1,' + >>> fix_filename("COM1.txt") + '_.txt' + >>> all('_' == fix_filename(chr(i)) for i in list(range(32))) + True + """ + return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", name, flags=re.IGNORECASE) From 40cb0d901f835203b6d62552a195c0020e8db4e2 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 10:44:44 -0400 Subject: [PATCH 21/26] Revert "Revert "Merge branch 'main' into argument-parsing"" This reverts commit 7412abcee5b61d9dcd7f3635ff5c27b4ccf540af. --- .github/workflows/pylint.yml | 4 +- CHANGELOG.md | 6 +++ README.md | 3 +- requirements.txt | 8 ++-- zspotify/album.py | 2 +- zspotify/const.py | 29 ++++++++++++- zspotify/playlist.py | 4 +- zspotify/podcast.py | 44 +++++++++++++------ zspotify/track.py | 82 ++++++++++++++++++++++++++---------- 9 files changed, 137 insertions(+), 45 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 0b88fe7a..c742569a 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -16,8 +16,8 @@ jobs: - name: Setup Pylint run: | python -m pip install --upgrade pip - pip install pylint + pip install pylint pylint-exit pip install -r requirements.txt - name: Run Pylint run: | - pylint $(git ls-files '*.py') + pylint $(git ls-files '*.py') || pylint-exit $? diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f93e54e..c7001d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ ## **Changelog:** +**v2.4 (27 Oct 20212):** +- Added realtime downloading support to avoid account suspensions. +- Fix for downloading by artist. +- Replace audio conversion method for better quality. +- Fix bug when automatically setting audio bitrate. + **v2.3 (25 Oct 2021):** - Moved changelog to seperate file. - Added argument parsing in search function (query results limit and query result types). diff --git a/README.md b/README.md index 979bafc0..e1b7dff7 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,8 @@ Create and run a container from the image: **There have been 2-3 reports from users who received account bans from Spotify for using this tool**. -We recommend using ZSpotify with a burner account. +We recommend using ZSpotify with a burner account. + Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus not appearing suspicious to Spotify. This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account. diff --git a/requirements.txt b/requirements.txt index 4f4b877e..b1877ecf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ +ffmpy git+https://github.com/kokarare1212/librespot-python music_tag -pydub Pillow -tqdm -tabulate \ No newline at end of file +protobuf +pydub +tabulate +tqdm \ No newline at end of file diff --git a/zspotify/album.py b/zspotify/album.py index 19004d11..2070da24 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -33,7 +33,7 @@ def get_album_name(album_id): def get_artist_albums(artist_id): """ Returns artist's albums """ - resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums') + resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums?include_groups=album%2Csingle') # Return a list each album's id album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] # Recursive requests to get all albums including singles an EPs diff --git a/zspotify/const.py b/zspotify/const.py index 3e4d9b0f..1ec7594a 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -96,14 +96,39 @@ CHUNK_SIZE = 'CHUNK_SIZE' SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' +DOWNLOAD_REAL_TIME = 'DOWNLOAD_REAL_TIME' + +BITRATE = 'BITRATE' + +CODEC_MAP = { + 'aac': 'aac', + 'fdk_aac': 'libfdk_aac', + 'm4a': 'aac', + 'mp3': 'libmp3lame', + 'ogg': 'copy', + 'opus': 'libopus', + 'vorbis': 'copy', +} + +EXT_MAP = { + 'aac': 'm4a', + 'fdk_aac': 'm4a', + 'm4a': 'm4a', + 'mp3': 'mp3', + 'ogg': 'ogg', + 'opus': 'ogg', + 'vorbis': 'ogg', +} + CONFIG_DEFAULT_SETTINGS = { 'ROOT_PATH': '../ZSpotify Music/', 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', 'SKIP_EXISTING_FILES': True, - 'DOWNLOAD_FORMAT': 'mp3', + 'DOWNLOAD_FORMAT': 'ogg', 'FORCE_PREMIUM': False, 'ANTI_BAN_WAIT_TIME': 1, 'OVERRIDE_AUTO_WAIT': False, 'CHUNK_SIZE': 50000, - 'SPLIT_ALBUM_DISCS': False + 'SPLIT_ALBUM_DISCS': False, + 'DOWNLOAD_REAL_TIME': False } diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 9a1304c2..cf333d1b 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -52,10 +52,12 @@ def download_playlist(playlist): playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) + enum = 1 for song in p_bar: download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', - disable_progressbar=True) + prefix=True, prefix_value=str(enum) ,disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) + enum += 1 def download_from_user_playlist(): diff --git a/zspotify/podcast.py b/zspotify/podcast.py index eea36c1b..03bc7b35 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -5,11 +5,11 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality from librespot.metadata import EpisodeId from tqdm import tqdm -from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE -from utils import sanitize_data, create_download_directory, MusicFormat +from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW, + SKIP_EXISTING_FILES) +from utils import create_download_directory, sanitize_data from zspotify import ZSpotify - EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' SHOWS_URL = 'https://api.spotify.com/v1/shows' @@ -50,21 +50,41 @@ def download_episode(episode_id) -> None: episode_id = EpisodeId.from_base62(episode_id) stream = ZSpotify.get_content_stream(episode_id, ZSpotify.DOWNLOAD_QUALITY) - download_directory = os.path.dirname(__file__) + ZSpotify.get_config(ROOT_PODCAST_PATH) + extra_paths + download_directory = os.path.join( + os.path.dirname(__file__), + ZSpotify.get_config(ROOT_PODCAST_PATH), + extra_paths, + ) + download_directory = os.path.realpath(download_directory) create_download_directory(download_directory) total_size = stream.input_stream.size - with open(download_directory + filename + MusicFormat.OGG.value, - 'wb') as file, tqdm( - desc=filename, - total=total_size, - unit='B', - unit_scale=True, - unit_divisor=1024 + + filepath = os.path.join(download_directory, f"{filename}.ogg") + if ( + os.path.isfile(filepath) + and os.path.getsize(filepath) == total_size + and ZSpotify.get_config(SKIP_EXISTING_FILES) + ): + print( + "\n### SKIPPING:", + podcast_name, + "-", + episode_name, + "(EPISODE ALREADY EXISTS) ###", + ) + return + + with open(filepath, 'wb') as file, tqdm( + desc=filename, + total=total_size, + unit='B', + unit_scale=True, + unit_divisor=1024 ) as bar: for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): bar.update(file.write( stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) # convert_audio_format(ROOT_PODCAST_PATH + - # extra_paths + filename + '.ogg') \ No newline at end of file + # extra_paths + filename + '.ogg') diff --git a/zspotify/track.py b/zspotify/track.py index 423cce0c..57960496 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -4,14 +4,14 @@ from typing import Any, Tuple, List from librespot.audio.decoders import AudioQuality from librespot.metadata import TrackId +from ffmpy import FFmpeg from pydub import AudioSegment from tqdm import tqdm from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \ - SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT -from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory, \ - MusicFormat + SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, BITRATE, CODEC_MAP, EXT_MAP, DOWNLOAD_REAL_TIME +from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory from zspotify import ZSpotify @@ -72,7 +72,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', ) else f'{prefix_value} - {song_name}' filename = os.path.join( - download_directory, f'{song_name}.{ZSpotify.get_config(DOWNLOAD_FORMAT)}') + download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.get_config(DOWNLOAD_FORMAT))}') except Exception as e: print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') @@ -81,11 +81,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', try: if not is_playable: print('\n### SKIPPING:', song_name, - '(SONG IS UNAVAILABLE) ###') + '(SONG IS UNAVAILABLE) ###') else: if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES): print('\n### SKIPPING:', song_name, - '(SONG ALREADY EXISTS) ###') + '(SONG ALREADY EXISTS) ###') else: if track_id != scraped_song_id: track_id = scraped_song_id @@ -103,15 +103,21 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', unit_divisor=1024, disable=disable_progressbar ) as p_bar: - for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): - p_bar.update(file.write( - stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)))) + for chunk in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): + data = stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE)) + if data == b'': + break + p_bar.update(file.write(data)) + if ZSpotify.get_config(DOWNLOAD_REAL_TIME): + if chunk == 0: + pause = get_segment_duration(p_bar) + if pause: + time.sleep(pause) - if ZSpotify.get_config(DOWNLOAD_FORMAT) == 'mp3': - convert_audio_format(filename) - set_audio_tags(filename, artists, name, album_name, - release_year, disc_number, track_number) - set_music_thumbnail(filename, image_url) + convert_audio_format(filename) + set_audio_tags(filename, artists, name, album_name, + release_year, disc_number, track_number) + set_music_thumbnail(filename, image_url) if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT): time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME)) @@ -123,14 +129,44 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', os.remove(filename) +def get_segment_duration(segment): + """ Returns playback duration of given audio segment """ + sound = AudioSegment( + data = segment, + sample_width = 2, + frame_rate = 44100, + channels = 2 + ) + duration = len(sound) / 5000 + return duration + + def convert_audio_format(filename) -> None: - """ Converts raw audio into playable mp3 """ - # print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###') - raw_audio = AudioSegment.from_file(filename, format=MusicFormat.OGG.value, - frame_rate=44100, channels=2, sample_width=2) - if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: - bitrate = '320k' + """ Converts raw audio into playable file """ + temp_filename = f'{os.path.splitext(filename)[0]}.tmp' + os.replace(filename, temp_filename) + + download_format = ZSpotify.get_config(DOWNLOAD_FORMAT) + file_codec = CODEC_MAP.get(download_format, "copy") + if file_codec != 'copy': + bitrate = ZSpotify.get_config(BITRATE) + if not bitrate: + if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH: + bitrate = '320k' + else: + bitrate = '160k' else: - bitrate = '160k' - raw_audio.export(filename, format=ZSpotify.get_config( - DOWNLOAD_FORMAT), bitrate=bitrate) + bitrate = None + + output_params = ['-c:a', file_codec] + if bitrate: + output_params += ['-b:a', bitrate] + + ff_m = FFmpeg( + global_options=['-y', '-hide_banner', '-loglevel error'], + inputs={temp_filename: None}, + outputs={filename: output_params} + ) + ff_m.run() + if os.path.exists(temp_filename): + os.remove(temp_filename) From 17329d8cf664fccca7198f458ad87b7c68e0d337 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 11:30:50 -0400 Subject: [PATCH 22/26] remade changes to updated main --- README.md | 20 +++++++++++++--- zspotify/app.py | 63 ++++++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e1b7dff7..825ae407 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ ![Stars](https://img.shields.io/github/stars/Footsiefat/zspotify.svg) ![Forks](https://img.shields.io/github/forks/Footsiefat/zspotify.svg) ![Size](https://img.shields.io/github/repo-size/Footsiefat/zspotify) + # ZSpotify ### A Spotify downloader needing only a python interpreter and ffmpeg. +

[Discord Server](https://discord.gg/skVNQKtyFq) - [Matrix Server](https://matrix.to/#/#zspotify:matrix.org) - [Gitea Mirror](https://git.robinsmediateam.dev/Footsiefat/zspotify) - [Main Site](https://footsiefat.github.io/) + ``` Requirements: @@ -27,6 +30,9 @@ Python packages: \*ffmpeg can be installed via apt for Debian-based distros or by downloading the binaries from [ffmpeg.org](https://ffmpeg.org) and placing them in your %PATH% in Windows. Mac users can install it with [Homebrew](https://brew.sh) by running `brew install ffmpeg`. \*\*Git can be installed via apt for Debian-based distros or by downloading the binaries from [git-scm.com](https://git-scm.com/download/win) for Windows. + +### Command line usage: + ``` Basic command line usage: python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. @@ -54,7 +60,9 @@ Options that can be configured in zs_config.json: ### Docker Usage ``` -Build the docker image from the Dockerfile: +Pull the official docker image (automatically updates): + docker pull cooper7692/zspotify-docker +Or build the docker image yourself from the Dockerfile: docker build -t zspotify . Create and run a container from the image: docker run --rm -v "$PWD/ZSpotify Music:/ZSpotify Music" -v "$PWD/ZSpotify Podcasts:/ZSpotify Podcasts" -it zspotify @@ -66,18 +74,24 @@ Create and run a container from the image: **There have been 2-3 reports from users who received account bans from Spotify for using this tool**. -We recommend using ZSpotify with a burner account. - +We recommend using ZSpotify with a burner account. Alternatively, there is a configuration option labled ```DOWNLOAD_REAL_TIME```, this limits the download speed to the duration of the song being downloaded thus not appearing suspicious to Spotify. This option is much slower and is only recommended for premium users who wish to download songs in 320kbps without buying premium on a burner account. **Use ZSpotify at your own risk**, the developers of ZSpotify are not responsible if your account gets banned. ### What do I do if I see "Your session has been terminated"? + If you see this, don't worry! Just try logging back in. If you see the incorrect username or password error, reset your password and you should be able to log back in and continue using Spotify. ### Contributing + Please refer to [CONTRIBUTING](CONTRIBUTING.md) ### Changelog + Please refer to [CHANGELOG](CHANGELOG.md) + +### Common Errors + +Please refer to [COMMON_ERRORS](COMMON_ERRORS.md) diff --git a/zspotify/app.py b/zspotify/app.py index 0c029d03..ad184df9 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -30,7 +30,8 @@ def client(args) -> None: ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH if args.url: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(args.url) + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + args.url) if track_id is not None: download_track(track_id) @@ -43,7 +44,7 @@ def client(args) -> None: name, _ = get_playlist_info(playlist_id) for song in playlist_songs: download_track(song[TRACK][ID], - sanitize_data(name) + '/') + sanitize_data(name) + '/') print('\n') elif episode_id is not None: download_episode(episode_id) @@ -57,7 +58,8 @@ def client(args) -> None: if args.liked_songs: for song in get_saved_tracks(): if not song[TRACK][NAME]: - print('### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') + print( + '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###') else: download_track(song[TRACK][ID], 'Liked Songs/') print('\n') @@ -67,7 +69,8 @@ def client(args) -> None: while len(search_text) == 0: search_text = input('Enter search or URL: ') - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls(search_text) + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + search_text) if track_id is not None: download_track(track_id) @@ -117,7 +120,6 @@ def search(search_term): raise ValueError('Invalid limit passed. Max is 50.\n') params['limit'] = splits[index+1] - if split == '-t' or split == '-type': allowed_types = ['track', 'playlist', 'album', 'artist'] @@ -151,6 +153,7 @@ def search(search_term): counter = 1 dics = [] + total_tracks = 0 if TRACK in params['type'].split(','): tracks = resp[TRACKS][ITEMS] if len(tracks) > 0: @@ -165,20 +168,20 @@ def search(search_term): track_data.append([counter, f'{track[NAME]} {explicit}', ','.join([artist[NAME] for artist in track[ARTISTS]])]) dics.append({ - ID : track[ID], - NAME : track[NAME], - 'type' : TRACK, + ID: track[ID], + NAME: track[NAME], + 'type': TRACK, }) counter += 1 total_tracks = counter - 1 - print(tabulate(track_data, headers=['S.NO', 'Name', 'Artists'], tablefmt='pretty')) + print(tabulate(track_data, headers=[ + 'S.NO', 'Name', 'Artists'], tablefmt='pretty')) print('\n') del tracks del track_data - else: - total_tracks = 0 + total_albums = 0 if ALBUM in params['type'].split(','): albums = resp[ALBUMS][ITEMS] if len(albums) > 0: @@ -188,20 +191,20 @@ def search(search_term): album_data.append([counter, album[NAME], ','.join([artist[NAME] for artist in album[ARTISTS]])]) dics.append({ - ID : album[ID], - NAME : album[NAME], - 'type' : ALBUM, + ID: album[ID], + NAME: album[NAME], + 'type': ALBUM, }) counter += 1 total_albums = counter - total_tracks - 1 - print(tabulate(album_data, headers=['S.NO', 'Album', 'Artists'], tablefmt='pretty')) + print(tabulate(album_data, headers=[ + 'S.NO', 'Album', 'Artists'], tablefmt='pretty')) print('\n') del albums del album_data - else: - total_albums = 0 + total_artists = 0 if ARTIST in params['type'].split(','): artists = resp[ARTISTS][ITEMS] if len(artists) > 0: @@ -210,39 +213,39 @@ def search(search_term): for artist in artists: artist_data.append([counter, artist[NAME]]) dics.append({ - ID : artist[ID], - NAME : artist[NAME], - 'type' : ARTIST, + ID: artist[ID], + NAME: artist[NAME], + 'type': ARTIST, }) counter += 1 total_artists = counter - total_tracks - total_albums - 1 - print(tabulate(artist_data, headers=['S.NO', 'Name'], tablefmt='pretty')) + print(tabulate(artist_data, headers=[ + 'S.NO', 'Name'], tablefmt='pretty')) print('\n') del artists del artist_data - else: - total_artists = 0 + total_playlists = 0 if PLAYLIST in params['type'].split(','): playlists = resp[PLAYLISTS][ITEMS] if len(playlists) > 0: print('### PLAYLISTS ###') playlist_data = [] for playlist in playlists: - playlist_data.append([counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) + playlist_data.append( + [counter, playlist[NAME], playlist[OWNER][DISPLAY_NAME]]) dics.append({ - ID : playlist[ID], - NAME : playlist[NAME], - 'type' : PLAYLIST, + ID: playlist[ID], + NAME: playlist[NAME], + 'type': PLAYLIST, }) counter += 1 total_playlists = counter - total_artists - total_tracks - total_albums - 1 - print(tabulate(playlist_data, headers=['S.NO', 'Name', 'Owner'], tablefmt='pretty')) + print(tabulate(playlist_data, headers=[ + 'S.NO', 'Name', 'Owner'], tablefmt='pretty')) print('\n') del playlists del playlist_data - else: - total_playlists = 0 if total_tracks + total_albums + total_artists + total_playlists == 0: print('NO RESULTS FOUND - EXITING...') From a2fb09e2ed1c096db5ad7ee95775a70f5e23fce5 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 12:59:36 -0400 Subject: [PATCH 23/26] Add support for multiple urls Fix #149 --- README.md | 2 +- zspotify/__main__.py | 9 ++++++--- zspotify/app.py | 43 ++++++++++++++++++++++--------------------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 825ae407..dfe32013 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Python packages: ``` Basic command line usage: - python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. + python zspotify Downloads the track, album, playlist or podcast episode specified as a command line argument. If an artist url is given, all albums by specified artist will be downloaded. Can take multiple urls. Extra command line options: -p, --playlist Downloads a saved playlist from your account diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 875b7676..2064e153 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -11,11 +11,12 @@ if __name__ == '__main__': action='store_true', help='Suppress the splash screen when loading.') group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('url', + group.add_argument('urls', type=str, + # action='extend', default='', - nargs='?', - help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url.') + nargs='*', + help='Downloads the track, album, playlist, podcast episode, or all albums by an artist from a url. Can take multiple urls.') group.add_argument('-ls', '--liked-songs', dest='liked_songs', action='store_true', @@ -32,3 +33,5 @@ if __name__ == '__main__': args = parser.parse_args() args.func(args) + + # print(args) diff --git a/zspotify/app.py b/zspotify/app.py index ad184df9..b0cbb968 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -29,28 +29,29 @@ def client(args) -> None: print('[ DETECTED FREE ACCOUNT - USING HIGH QUALITY ]\n\n') ZSpotify.DOWNLOAD_QUALITY = AudioQuality.HIGH - if args.url: - track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( - args.url) + if args.urls: + for spotify_url in args.urls: + track_id, album_id, playlist_id, episode_id, show_id, artist_id = regex_input_for_urls( + spotify_url) - if track_id is not None: - download_track(track_id) - elif artist_id is not None: - download_artist_albums(artist_id) - elif album_id is not None: - download_album(album_id) - elif playlist_id is not None: - playlist_songs = get_playlist_songs(playlist_id) - name, _ = get_playlist_info(playlist_id) - for song in playlist_songs: - download_track(song[TRACK][ID], - sanitize_data(name) + '/') - print('\n') - elif episode_id is not None: - download_episode(episode_id) - elif show_id is not None: - for episode in get_show_episodes(show_id): - download_episode(episode) + if track_id is not None: + download_track(track_id) + elif artist_id is not None: + download_artist_albums(artist_id) + elif album_id is not None: + download_album(album_id) + elif playlist_id is not None: + playlist_songs = get_playlist_songs(playlist_id) + name, _ = get_playlist_info(playlist_id) + for song in playlist_songs: + download_track(song[TRACK][ID], + sanitize_data(name) + '/') + print('\n') + elif episode_id is not None: + download_episode(episode_id) + elif show_id is not None: + for episode in get_show_episodes(show_id): + download_episode(episode) if args.playlist: download_from_user_playlist() From dd9f243610ac369765056aa8cfa1270dfff1eb19 Mon Sep 17 00:00:00 2001 From: el-gringo-alto <22480930+el-gringo-alto@users.noreply.github.com> Date: Thu, 28 Oct 2021 13:23:12 -0400 Subject: [PATCH 24/26] Update __main__.py --- zspotify/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zspotify/__main__.py b/zspotify/__main__.py index 2064e153..6016faf6 100644 --- a/zspotify/__main__.py +++ b/zspotify/__main__.py @@ -33,5 +33,3 @@ if __name__ == '__main__': args = parser.parse_args() args.func(args) - - # print(args) From 0dd2802ba2b194b2fa86958951360d02bcb842c3 Mon Sep 17 00:00:00 2001 From: logykk Date: Sat, 30 Oct 2021 22:43:07 +1300 Subject: [PATCH 25/26] Replaced filename string sanitization --- zspotify/album.py | 4 ++-- zspotify/app.py | 6 +++--- zspotify/const.py | 2 -- zspotify/playlist.py | 4 ++-- zspotify/podcast.py | 4 ++-- zspotify/track.py | 10 +++++----- zspotify/utils.py | 7 ------- 7 files changed, 14 insertions(+), 23 deletions(-) diff --git a/zspotify/album.py b/zspotify/album.py index 8a38b6e5..5325caa9 100644 --- a/zspotify/album.py +++ b/zspotify/album.py @@ -2,7 +2,7 @@ from tqdm import tqdm from const import ITEMS, ARTISTS, NAME, ID from track import download_track -from utils import sanitize_data, fix_filename +from utils import fix_filename from zspotify import ZSpotify ALBUM_URL = 'https://api.spotify.com/v1/albums' @@ -28,7 +28,7 @@ def get_album_tracks(album_id): def get_album_name(album_id): """ Returns album name """ resp = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}') - return resp[ARTISTS][0][NAME], sanitize_data(resp[NAME]) + return resp[ARTISTS][0][NAME], fix_filename(resp[NAME]) def get_artist_albums(artist_id): diff --git a/zspotify/app.py b/zspotify/app.py index b0cbb968..e0511cea 100644 --- a/zspotify/app.py +++ b/zspotify/app.py @@ -7,7 +7,7 @@ from const import TRACK, NAME, ID, ARTIST, ARTISTS, ITEMS, TRACKS, EXPLICIT, ALB from playlist import get_playlist_songs, get_playlist_info, download_from_user_playlist, download_playlist from podcast import download_episode, get_show_episodes from track import download_track, get_saved_tracks -from utils import sanitize_data, splash, split_input, regex_input_for_urls +from utils import fix_filename, splash, split_input, regex_input_for_urls from zspotify import ZSpotify SEARCH_URL = 'https://api.spotify.com/v1/search' @@ -45,7 +45,7 @@ def client(args) -> None: name, _ = get_playlist_info(playlist_id) for song in playlist_songs: download_track(song[TRACK][ID], - sanitize_data(name) + '/') + fix_filename(name) + '/') print('\n') elif episode_id is not None: download_episode(episode_id) @@ -83,7 +83,7 @@ def client(args) -> None: playlist_songs = get_playlist_songs(playlist_id) name, _ = get_playlist_info(playlist_id) for song in playlist_songs: - download_track(song[TRACK][ID], sanitize_data(name) + '/') + download_track(song[TRACK][ID], fix_filename(name) + '/') print('\n') elif episode_id is not None: download_episode(episode_id) diff --git a/zspotify/const.py b/zspotify/const.py index 73c5e9c1..39bf059f 100644 --- a/zspotify/const.py +++ b/zspotify/const.py @@ -1,5 +1,3 @@ -SANITIZE = ('\\', '/', ':', '*', '?', '\'', '<', '>', '"') - SAVED_TRACKS_URL = 'https://api.spotify.com/v1/me/tracks' TRACKS_URL = 'https://api.spotify.com/v1/tracks' diff --git a/zspotify/playlist.py b/zspotify/playlist.py index 44156887..d323723e 100644 --- a/zspotify/playlist.py +++ b/zspotify/playlist.py @@ -2,7 +2,7 @@ from tqdm import tqdm from const import ITEMS, ID, TRACK, NAME from track import download_track -from utils import sanitize_data +from utils import fix_filename from zspotify import ZSpotify MY_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists' @@ -54,7 +54,7 @@ def download_playlist(playlist): p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True) enum = 1 for song in p_bar: - download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', + download_track(song[TRACK][ID], fix_filename(playlist[NAME].strip()) + '/', prefix=True, prefix_value=str(enum) ,disable_progressbar=True) p_bar.set_description(song[TRACK][NAME]) enum += 1 diff --git a/zspotify/podcast.py b/zspotify/podcast.py index 03bc7b35..686ea4ab 100644 --- a/zspotify/podcast.py +++ b/zspotify/podcast.py @@ -7,7 +7,7 @@ from tqdm import tqdm from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW, SKIP_EXISTING_FILES) -from utils import create_download_directory, sanitize_data +from utils import create_download_directory, fix_filename from zspotify import ZSpotify EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' @@ -18,7 +18,7 @@ def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]: info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}') if ERROR in info: return None, None - return sanitize_data(info[SHOW][NAME]), sanitize_data(info[NAME]) + return fix_filename(info[SHOW][NAME]), fix_filename(info[NAME]) def get_show_episodes(show_id_str) -> list: diff --git a/zspotify/track.py b/zspotify/track.py index 6f01c38f..b99a3fdd 100644 --- a/zspotify/track.py +++ b/zspotify/track.py @@ -11,7 +11,7 @@ from tqdm import tqdm from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, SPLIT_ALBUM_DISCS, ROOT_PATH, DOWNLOAD_FORMAT, CHUNK_SIZE, \ SKIP_EXISTING_FILES, ANTI_BAN_WAIT_TIME, OVERRIDE_AUTO_WAIT, BITRATE, CODEC_MAP, EXT_MAP, DOWNLOAD_REAL_TIME -from utils import sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory +from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory from zspotify import ZSpotify @@ -38,9 +38,9 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any artists = [] for data in info[TRACKS][0][ARTISTS]: - artists.append(sanitize_data(data[NAME])) - album_name = sanitize_data(info[TRACKS][0][ALBUM][NAME]) - name = sanitize_data(info[TRACKS][0][NAME]) + artists.append(data[NAME]) + album_name = info[TRACKS][0][ALBUM][NAME] + name = info[TRACKS][0][NAME] image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL] release_year = info[TRACKS][0][ALBUM][RELEASE_DATE].split('-')[0] disc_number = info[TRACKS][0][DISC_NUMBER] @@ -66,7 +66,7 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', download_directory = os.path.join(os.path.dirname( __file__), ZSpotify.get_config(ROOT_PATH), extra_paths) - song_name = artists[0] + ' - ' + name + song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name) if prefix: song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit( ) else f'{prefix_value} - {song_name}' diff --git a/zspotify/utils.py b/zspotify/utils.py index f6c95840..36c9edaf 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -60,13 +60,6 @@ def clear() -> None: os.system('clear') -def sanitize_data(value) -> str: - """ Returns given string with problematic removed """ - for pattern in SANITIZE: - value = value.replace(pattern, '') - return value.replace('|', '-') - - def set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number) -> None: """ sets music_tag metadata """ tags = music_tag.load_file(filename) From c945aa9a09b1841dbf2f92be8b3cd5d7afd01c5a Mon Sep 17 00:00:00 2001 From: logykk Date: Sat, 30 Oct 2021 23:02:36 +1300 Subject: [PATCH 26/26] Stopped importing something that doesn't exist --- zspotify/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zspotify/utils.py b/zspotify/utils.py index 36c9edaf..3ec4ef9d 100644 --- a/zspotify/utils.py +++ b/zspotify/utils.py @@ -8,7 +8,7 @@ from typing import List, Tuple import music_tag import requests -from const import SANITIZE, ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ +from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \ WINDOWS_SYSTEM