mirror of
https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
synced 2024-11-26 09:53:17 +01:00
Merge branch 'main' into argument-parsing
This commit is contained in:
commit
e7b3f5c4bd
4
.github/workflows/pylint.yml
vendored
4
.github/workflows/pylint.yml
vendored
@ -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 $?
|
||||||
|
@ -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).
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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():
|
||||||
|
@ -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,17 +50,37 @@ 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")
|
||||||
desc=filename,
|
if (
|
||||||
total=total_size,
|
os.path.isfile(filepath)
|
||||||
unit='B',
|
and os.path.getsize(filepath) == total_size
|
||||||
unit_scale=True,
|
and ZSpotify.get_config(SKIP_EXISTING_FILES)
|
||||||
unit_divisor=1024
|
):
|
||||||
|
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:
|
) as bar:
|
||||||
for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1):
|
for _ in range(int(total_size / ZSpotify.get_config(CHUNK_SIZE)) + 1):
|
||||||
bar.update(file.write(
|
bar.update(file.write(
|
||||||
|
@ -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 ###')
|
||||||
@ -81,11 +81,11 @@ def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='',
|
|||||||
try:
|
try:
|
||||||
if not is_playable:
|
if not is_playable:
|
||||||
print('\n### SKIPPING:', song_name,
|
print('\n### SKIPPING:', song_name,
|
||||||
'(SONG IS UNAVAILABLE) ###')
|
'(SONG IS UNAVAILABLE) ###')
|
||||||
else:
|
else:
|
||||||
if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES):
|
if os.path.isfile(filename) and os.path.getsize(filename) and ZSpotify.get_config(SKIP_EXISTING_FILES):
|
||||||
print('\n### SKIPPING:', song_name,
|
print('\n### SKIPPING:', song_name,
|
||||||
'(SONG ALREADY EXISTS) ###')
|
'(SONG ALREADY EXISTS) ###')
|
||||||
else:
|
else:
|
||||||
if track_id != scraped_song_id:
|
if track_id != scraped_song_id:
|
||||||
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,
|
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)
|
set_music_thumbnail(filename, image_url)
|
||||||
set_music_thumbnail(filename, image_url)
|
|
||||||
|
|
||||||
if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT):
|
if not ZSpotify.get_config(OVERRIDE_AUTO_WAIT):
|
||||||
time.sleep(ZSpotify.get_config(ANTI_BAN_WAIT_TIME))
|
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)
|
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)
|
|
||||||
if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH:
|
download_format = ZSpotify.get_config(DOWNLOAD_FORMAT)
|
||||||
bitrate = '320k'
|
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:
|
else:
|
||||||
bitrate = '160k'
|
bitrate = None
|
||||||
raw_audio.export(filename, format=ZSpotify.get_config(
|
|
||||||
DOWNLOAD_FORMAT), bitrate=bitrate)
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user