2021-10-23 17:36:37 +02:00
|
|
|
import os
|
2021-10-30 14:58:09 +02:00
|
|
|
import re
|
2021-10-23 17:36:37 +02:00
|
|
|
import time
|
2021-10-24 19:45:52 +02:00
|
|
|
from typing import Any, Tuple, List
|
2021-10-23 17:36:37 +02:00
|
|
|
|
2021-10-26 22:14:31 +02:00
|
|
|
from librespot.audio.decoders import AudioQuality
|
2021-10-23 17:36:37 +02:00
|
|
|
from librespot.metadata import TrackId
|
2021-10-26 21:06:17 +02:00
|
|
|
from ffmpy import FFmpeg
|
2021-10-27 11:49:21 +02:00
|
|
|
from pydub import AudioSegment
|
2021-10-23 17:36:37 +02:00
|
|
|
from tqdm import tqdm
|
|
|
|
|
|
|
|
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \
|
2021-11-18 22:16:07 +01:00
|
|
|
RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS
|
2021-10-30 23:22:50 +02:00
|
|
|
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
|
2021-11-09 17:46:28 +01:00
|
|
|
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive
|
2021-10-23 17:36:37 +02:00
|
|
|
from zspotify import ZSpotify
|
|
|
|
|
|
|
|
|
|
|
|
def get_saved_tracks() -> list:
|
|
|
|
""" Returns user's saved tracks """
|
|
|
|
songs = []
|
|
|
|
offset = 0
|
|
|
|
limit = 50
|
|
|
|
|
|
|
|
while True:
|
2021-10-25 06:08:56 +02:00
|
|
|
resp = ZSpotify.invoke_url_with_params(
|
|
|
|
SAVED_TRACKS_URL, limit=limit, offset=offset)
|
2021-10-23 17:36:37 +02:00
|
|
|
offset += limit
|
|
|
|
songs.extend(resp[ITEMS])
|
|
|
|
if len(resp[ITEMS]) < limit:
|
|
|
|
break
|
|
|
|
|
|
|
|
return songs
|
|
|
|
|
|
|
|
|
2021-11-13 06:06:34 +01:00
|
|
|
def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any, int]:
|
2021-10-23 17:36:37 +02:00
|
|
|
""" Retrieves metadata for downloaded songs """
|
|
|
|
info = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token')
|
|
|
|
|
|
|
|
artists = []
|
|
|
|
for data in info[TRACKS][0][ARTISTS]:
|
2021-10-30 11:43:07 +02:00
|
|
|
artists.append(data[NAME])
|
|
|
|
album_name = info[TRACKS][0][ALBUM][NAME]
|
|
|
|
name = info[TRACKS][0][NAME]
|
2021-10-23 17:36:37 +02:00
|
|
|
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]
|
|
|
|
track_number = info[TRACKS][0][TRACK_NUMBER]
|
|
|
|
scraped_song_id = info[TRACKS][0][ID]
|
|
|
|
is_playable = info[TRACKS][0][IS_PLAYABLE]
|
2021-11-13 06:06:34 +01:00
|
|
|
duration_ms = info[TRACKS][0][DURATION_MS]
|
2021-10-23 17:36:37 +02:00
|
|
|
|
2021-11-13 06:06:34 +01:00
|
|
|
return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms
|
2021-10-23 17:36:37 +02:00
|
|
|
|
2021-10-31 19:55:00 +01:00
|
|
|
def get_song_duration(song_id: str) -> float:
|
|
|
|
""" Retrieves duration of song in second as is on spotify """
|
|
|
|
|
|
|
|
resp = ZSpotify.invoke_url(f'{TRACK_STATS_URL}{song_id}')
|
|
|
|
|
|
|
|
# get duration in miliseconds
|
|
|
|
ms_duration = resp['duration_ms']
|
|
|
|
# convert to seconds
|
|
|
|
duration = float(ms_duration)/1000
|
|
|
|
|
|
|
|
# debug
|
2021-10-31 20:00:14 +01:00
|
|
|
# print(duration)
|
|
|
|
# print(type(duration))
|
2021-10-31 19:55:00 +01:00
|
|
|
|
|
|
|
return duration
|
2021-10-23 17:36:37 +02:00
|
|
|
|
|
|
|
# noinspection PyBroadException
|
2021-10-25 07:36:17 +02:00
|
|
|
def download_track(track_id: str, extra_paths='', prefix=False, prefix_value='', disable_progressbar=False) -> None:
|
2021-10-23 17:36:37 +02:00
|
|
|
""" Downloads raw song audio from Spotify """
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-10-23 17:36:37 +02:00
|
|
|
try:
|
|
|
|
(artists, album_name, name, image_url, release_year, disc_number,
|
2021-11-13 06:06:34 +01:00
|
|
|
track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id)
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-11-18 22:39:17 +01:00
|
|
|
if ZSpotify.CONFIG.get_split_album_discs():
|
2021-10-25 07:36:17 +02:00
|
|
|
download_directory = os.path.join(os.path.dirname(
|
2021-11-18 22:39:17 +01:00
|
|
|
__file__), ZSpotify.CONFIG.get_root_path(), extra_paths, f'Disc {disc_number}')
|
2021-10-25 07:36:17 +02:00
|
|
|
else:
|
|
|
|
download_directory = os.path.join(os.path.dirname(
|
2021-11-18 22:39:17 +01:00
|
|
|
__file__), ZSpotify.CONFIG.get_root_path(), extra_paths)
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-10-30 11:43:07 +02:00
|
|
|
song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name)
|
2021-10-25 07:36:17 +02:00
|
|
|
if prefix:
|
|
|
|
song_name = f'{prefix_value.zfill(2)} - {song_name}' if prefix_value.isdigit(
|
|
|
|
) else f'{prefix_value} - {song_name}'
|
|
|
|
|
|
|
|
filename = os.path.join(
|
2021-11-18 22:39:17 +01:00
|
|
|
download_directory, f'{song_name}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())}')
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-10-30 14:35:01 +02:00
|
|
|
check_name = os.path.isfile(filename) and os.path.getsize(filename)
|
|
|
|
check_id = scraped_song_id in get_directory_song_ids(download_directory)
|
2021-11-18 23:24:08 +01:00
|
|
|
check_all_time = scraped_song_id in get_previously_downloaded()
|
2021-10-30 14:35:01 +02:00
|
|
|
|
|
|
|
# a song with the same name is installed
|
|
|
|
if not check_id and check_name:
|
2021-10-30 14:58:09 +02:00
|
|
|
c = len([file for file in os.listdir(download_directory)
|
|
|
|
if re.search(f'^{song_name}_', file)]) + 1
|
|
|
|
|
2021-10-30 14:35:01 +02:00
|
|
|
filename = os.path.join(
|
2021-11-18 22:39:17 +01:00
|
|
|
download_directory, f'{song_name}_{c}.{EXT_MAP.get(ZSpotify.CONFIG.get_download_format())}')
|
2021-10-30 14:35:01 +02:00
|
|
|
|
|
|
|
|
2021-10-25 07:36:17 +02:00
|
|
|
except Exception as e:
|
2021-10-23 17:36:37 +02:00
|
|
|
print('### SKIPPING SONG - FAILED TO QUERY METADATA ###')
|
2021-10-25 07:36:17 +02:00
|
|
|
print(e)
|
2021-10-23 17:36:37 +02:00
|
|
|
else:
|
|
|
|
try:
|
2021-10-25 07:36:17 +02:00
|
|
|
if not is_playable:
|
|
|
|
print('\n### SKIPPING:', song_name,
|
2021-10-27 11:49:21 +02:00
|
|
|
'(SONG IS UNAVAILABLE) ###')
|
2021-10-25 07:36:17 +02:00
|
|
|
else:
|
2021-11-18 22:39:17 +01:00
|
|
|
if check_id and check_name and ZSpotify.CONFIG.get_skip_existing_files():
|
2021-10-25 07:36:17 +02:00
|
|
|
print('\n### SKIPPING:', song_name,
|
2021-10-27 11:49:21 +02:00
|
|
|
'(SONG ALREADY EXISTS) ###')
|
2021-11-08 19:35:32 +01:00
|
|
|
|
2021-11-18 22:39:17 +01:00
|
|
|
elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded():
|
2021-11-08 19:35:32 +01:00
|
|
|
print('\n### SKIPPING:', song_name,
|
2021-11-09 18:03:44 +01:00
|
|
|
'(SONG ALREADY DOWNLOADED ONCE) ###')
|
2021-11-08 19:35:32 +01:00
|
|
|
|
2021-10-25 07:36:17 +02:00
|
|
|
else:
|
|
|
|
if track_id != scraped_song_id:
|
|
|
|
track_id = scraped_song_id
|
|
|
|
track_id = TrackId.from_base62(track_id)
|
|
|
|
stream = ZSpotify.get_content_stream(
|
|
|
|
track_id, ZSpotify.DOWNLOAD_QUALITY)
|
|
|
|
create_download_directory(download_directory)
|
|
|
|
total_size = stream.input_stream.size
|
|
|
|
|
|
|
|
with open(filename, 'wb') as file, tqdm(
|
|
|
|
desc=song_name,
|
|
|
|
total=total_size,
|
|
|
|
unit='B',
|
|
|
|
unit_scale=True,
|
|
|
|
unit_divisor=1024,
|
|
|
|
disable=disable_progressbar
|
|
|
|
) as p_bar:
|
2021-11-18 22:39:17 +01:00
|
|
|
pause = duration_ms / ZSpotify.CONFIG.get_chunk_size()
|
|
|
|
for chunk in range(int(total_size / ZSpotify.CONFIG.get_chunk_size()) + 1):
|
|
|
|
data = stream.input_stream.stream().read(ZSpotify.CONFIG.get_chunk_size())
|
2021-10-27 11:49:21 +02:00
|
|
|
p_bar.update(file.write(data))
|
2021-11-18 22:39:17 +01:00
|
|
|
if ZSpotify.CONFIG.get_download_real_time():
|
2021-11-13 06:06:34 +01:00
|
|
|
time.sleep(pause)
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-10-26 21:06:17 +02:00
|
|
|
convert_audio_format(filename)
|
|
|
|
set_audio_tags(filename, artists, name, album_name,
|
2021-10-27 11:49:21 +02:00
|
|
|
release_year, disc_number, track_number)
|
2021-10-26 21:06:17 +02:00
|
|
|
set_music_thumbnail(filename, image_url)
|
2021-10-25 07:36:17 +02:00
|
|
|
|
2021-11-08 19:35:32 +01:00
|
|
|
# add song id to archive file
|
2021-11-18 22:39:17 +01:00
|
|
|
if ZSpotify.CONFIG.get_skip_previously_downloaded():
|
2021-11-18 23:24:08 +01:00
|
|
|
add_to_archive(scraped_song_id, artists[0], name)
|
2021-10-30 14:35:01 +02:00
|
|
|
# add song id to download directory's .song_ids file
|
2021-10-30 14:42:59 +02:00
|
|
|
if not check_id:
|
|
|
|
add_to_directory_song_ids(download_directory, scraped_song_id)
|
2021-10-30 14:35:01 +02:00
|
|
|
|
2021-11-18 22:39:17 +01:00
|
|
|
if not ZSpotify.CONFIG.get_anti_ban_wait_time():
|
|
|
|
time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time())
|
2021-10-25 07:36:17 +02:00
|
|
|
except Exception as e:
|
2021-10-23 17:36:37 +02:00
|
|
|
print('### SKIPPING:', song_name,
|
|
|
|
'(GENERAL DOWNLOAD ERROR) ###')
|
2021-10-25 07:36:17 +02:00
|
|
|
print(e)
|
2021-10-23 17:36:37 +02:00
|
|
|
if os.path.exists(filename):
|
|
|
|
os.remove(filename)
|
|
|
|
|
|
|
|
|
|
|
|
def convert_audio_format(filename) -> None:
|
2021-10-26 21:06:17 +02:00
|
|
|
""" Converts raw audio into playable file """
|
|
|
|
temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
|
|
|
|
os.replace(filename, temp_filename)
|
|
|
|
|
2021-11-18 22:39:17 +01:00
|
|
|
download_format = ZSpotify.CONFIG.get_download_format().lower()
|
2021-11-17 09:29:15 +01:00
|
|
|
file_codec = CODEC_MAP.get(download_format, 'copy')
|
2021-10-26 21:06:17 +02:00
|
|
|
if file_codec != 'copy':
|
2021-11-18 22:39:17 +01:00
|
|
|
bitrate = ZSpotify.CONFIG.get_bitrate()
|
2021-10-26 22:14:31 +02:00
|
|
|
if not bitrate:
|
|
|
|
if ZSpotify.DOWNLOAD_QUALITY == AudioQuality.VERY_HIGH:
|
|
|
|
bitrate = '320k'
|
|
|
|
else:
|
|
|
|
bitrate = '160k'
|
2021-10-23 17:36:37 +02:00
|
|
|
else:
|
2021-10-26 21:06:17 +02:00
|
|
|
bitrate = None
|
|
|
|
|
2021-10-26 22:14:31 +02:00
|
|
|
output_params = ['-c:a', file_codec]
|
|
|
|
if bitrate:
|
|
|
|
output_params += ['-b:a', bitrate]
|
|
|
|
|
2021-10-26 21:06:17 +02:00
|
|
|
ff_m = FFmpeg(
|
|
|
|
global_options=['-y', '-hide_banner', '-loglevel error'],
|
|
|
|
inputs={temp_filename: None},
|
2021-10-26 22:14:31 +02:00
|
|
|
outputs={filename: output_params}
|
2021-10-26 21:06:17 +02:00
|
|
|
)
|
|
|
|
ff_m.run()
|
|
|
|
if os.path.exists(temp_filename):
|
|
|
|
os.remove(temp_filename)
|