Merge branch 'main' into argument-parsing

This commit is contained in:
el-gringo-alto 2021-10-28 02:19:06 -04:00 committed by GitHub
commit e7b3f5c4bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 45 deletions

View File

@ -16,8 +16,8 @@ jobs:
- name: Setup Pylint - name: Setup Pylint
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install pylint pip install pylint pylint-exit
pip install -r requirements.txt pip install -r requirements.txt
- name: Run Pylint - name: Run Pylint
run: | run: |
pylint $(git ls-files '*.py') pylint $(git ls-files '*.py') || pylint-exit $?

View File

@ -1,4 +1,10 @@
## **Changelog:** ## **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):** **v2.3 (25 Oct 2021):**
- Moved changelog to seperate file. - Moved changelog to seperate file.
- Added argument parsing in search function (query results limit and query result types). - Added argument parsing in search function (query results limit and query result types).

View File

@ -67,6 +67,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**. **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. 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. 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.

View File

@ -1,6 +1,8 @@
ffmpy
git+https://github.com/kokarare1212/librespot-python git+https://github.com/kokarare1212/librespot-python
music_tag music_tag
pydub
Pillow Pillow
tqdm protobuf
pydub
tabulate tabulate
tqdm

View File

@ -33,7 +33,7 @@ def get_album_name(album_id):
def get_artist_albums(artist_id): def get_artist_albums(artist_id):
""" Returns artist's albums """ """ 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 # Return a list each album's id
album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))] album_ids = [resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))]
# Recursive requests to get all albums including singles an EPs # Recursive requests to get all albums including singles an EPs

View File

@ -96,14 +96,39 @@ CHUNK_SIZE = 'CHUNK_SIZE'
SPLIT_ALBUM_DISCS = 'SPLIT_ALBUM_DISCS' 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 = { CONFIG_DEFAULT_SETTINGS = {
'ROOT_PATH': '../ZSpotify Music/', 'ROOT_PATH': '../ZSpotify Music/',
'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/', 'ROOT_PODCAST_PATH': '../ZSpotify Podcasts/',
'SKIP_EXISTING_FILES': True, 'SKIP_EXISTING_FILES': True,
'DOWNLOAD_FORMAT': 'mp3', 'DOWNLOAD_FORMAT': 'ogg',
'FORCE_PREMIUM': False, 'FORCE_PREMIUM': False,
'ANTI_BAN_WAIT_TIME': 1, 'ANTI_BAN_WAIT_TIME': 1,
'OVERRIDE_AUTO_WAIT': False, 'OVERRIDE_AUTO_WAIT': False,
'CHUNK_SIZE': 50000, 'CHUNK_SIZE': 50000,
'SPLIT_ALBUM_DISCS': False 'SPLIT_ALBUM_DISCS': False,
'DOWNLOAD_REAL_TIME': False
} }

View File

@ -52,10 +52,12 @@ def download_playlist(playlist):
playlist_songs = [song for song in get_playlist_songs(playlist[ID]) if song[TRACK][ID]] 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) p_bar = tqdm(playlist_songs, unit='song', total=len(playlist_songs), unit_scale=True)
enum = 1
for song in p_bar: for song in p_bar:
download_track(song[TRACK][ID], sanitize_data(playlist[NAME].strip()) + '/', 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]) p_bar.set_description(song[TRACK][NAME])
enum += 1
def download_from_user_playlist(): def download_from_user_playlist():

View File

@ -5,11 +5,11 @@ from librespot.audio.decoders import VorbisOnlyAudioQuality
from librespot.metadata import EpisodeId from librespot.metadata import EpisodeId
from tqdm import tqdm from tqdm import tqdm
from const import NAME, ERROR, SHOW, ITEMS, ID, ROOT_PODCAST_PATH, CHUNK_SIZE from const import (CHUNK_SIZE, ERROR, ID, ITEMS, NAME, ROOT_PODCAST_PATH, SHOW,
from utils import sanitize_data, create_download_directory, MusicFormat SKIP_EXISTING_FILES)
from utils import create_download_directory, sanitize_data
from zspotify import ZSpotify from zspotify import ZSpotify
EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes' EPISODE_INFO_URL = 'https://api.spotify.com/v1/episodes'
SHOWS_URL = 'https://api.spotify.com/v1/shows' SHOWS_URL = 'https://api.spotify.com/v1/shows'
@ -50,12 +50,32 @@ def download_episode(episode_id) -> None:
episode_id = EpisodeId.from_base62(episode_id) episode_id = EpisodeId.from_base62(episode_id)
stream = ZSpotify.get_content_stream(episode_id, ZSpotify.DOWNLOAD_QUALITY) 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) create_download_directory(download_directory)
total_size = stream.input_stream.size total_size = stream.input_stream.size
with open(download_directory + filename + MusicFormat.OGG.value,
'wb') as file, tqdm( 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, desc=filename,
total=total_size, total=total_size,
unit='B', unit='B',

View File

@ -4,14 +4,14 @@ from typing import Any, Tuple, List
from librespot.audio.decoders import AudioQuality from librespot.audio.decoders import AudioQuality
from librespot.metadata import TrackId from librespot.metadata import TrackId
from ffmpy import FFmpeg
from pydub import AudioSegment from pydub import AudioSegment
from tqdm import tqdm from tqdm import tqdm
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \ 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, \ 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 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 sanitize_data, set_audio_tags, set_music_thumbnail, create_download_directory
MusicFormat
from zspotify import ZSpotify 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}' ) else f'{prefix_value} - {song_name}'
filename = os.path.join( 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: except Exception as e:
print('### SKIPPING SONG - FAILED TO QUERY METADATA ###') print('### SKIPPING SONG - FAILED TO QUERY METADATA ###')
@ -103,11 +103,17 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
unit_divisor=1024, unit_divisor=1024,
disable=disable_progressbar disable=disable_progressbar
) as p_bar: ) as p_bar:
for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1): for chunk in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1):
p_bar.update(file.write( data = stream.input_stream.stream().read(ZSpotify.get_config(CHUNK_SIZE))
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) convert_audio_format(filename)
set_audio_tags(filename, artists, name, album_name, set_audio_tags(filename, artists, name, album_name,
release_year, disc_number, track_number) release_year, disc_number, track_number)
@ -123,14 +129,44 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
os.remove(filename) 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: def convert_audio_format(filename) -> None:
""" Converts raw audio into playable mp3 """ """ Converts raw audio into playable file """
# print('### CONVERTING TO ' + MUSIC_FORMAT.upper() + ' ###') temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
raw_audio = AudioSegment.from_file(filename, format=MusicFormat.OGG.value, os.replace(filename, temp_filename)
frame_rate=44100, channels=2, sample_width=2)
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: if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH:
bitrate = '320k' bitrate = '320k'
else: else:
bitrate = '160k' bitrate = '160k'
raw_audio.export(filename, format=ZSpotify.get_config( else:
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)