diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index c742569..0b88fe7 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 c7001d4..9f93e54 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 e1b7dff..979bafc 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 b1877ec..4f4b877 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 2070da2..19004d1 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 1ec7594..3e4d9b0 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 cf333d1..9a1304c 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 03bc7b3..eea36c1 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 5796049..423cce0 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)