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 master
This commit is contained in:
commit
5bf5a2656e
85
README.md
85
README.md
@ -37,26 +37,79 @@ Python packages:
|
||||
Basic command line usage:
|
||||
python zspotify <track/album/playlist/episode/artist url> 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
|
||||
Different usage modes:
|
||||
(nothing) Download the tracks/alumbs/playlists URLs from the parameter
|
||||
-d, --download Download all tracks/alumbs/playlists URLs from the specified file
|
||||
-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
|
||||
-s, --search Loads search prompt to find then download a specific track, album or playlist
|
||||
|
||||
Extra command line options:
|
||||
-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
|
||||
ROOT_PODCAST_PATH Change this path if you don't like the default directory where ZSpotify saves the podcasts
|
||||
|
||||
SKIP_EXISTING_FILES Set this to false if you want ZSpotify to overwrite files with the same name rather than skipping the song
|
||||
|
||||
MUSIC_FORMAT Can be "mp3" or "ogg", mp3 is required for track metadata however ogg is slightly higher quality as it is not transcoded.
|
||||
|
||||
FORCE_PREMIUM Set this to true if ZSpotify isn't automatically detecting that you are using a premium account
|
||||
|
||||
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
|
||||
--config-location Use a different zs_config.json, defaults to the one in the program directory
|
||||
```
|
||||
|
||||
### Options:
|
||||
|
||||
All these options can either be configured in the zs_config or via the commandline, in case of both the commandline-option has higher priority.
|
||||
Be aware you have to set boolean values in the commandline like this: `--download-real-time=True`
|
||||
|
||||
| Key (zs-config) | commandline parameter | Description
|
||||
|------------------------------|----------------------------------|---------------------------------------------------------------------|
|
||||
| ROOT_PATH | --root-path | directory where ZSpotify saves the music
|
||||
| ROOT_PODCAST_PATH | --root-podcast-path | directory where ZSpotify saves the podcasts
|
||||
| SKIP_EXISTING_FILES | --skip-existing-files | Skip songs with the same name
|
||||
| SKIP_PREVIOUSLY_DOWNLOADED | --skip-previously-downloaded | Create a .song_archive file and skip previously downloaded songs
|
||||
| DOWNLOAD_FORMAT | --download-format | The download audio format (aac, fdk_aac, m4a, mp3, ogg, opus, vorbis)
|
||||
| FORCE_PREMIUM | --force-premium | Force the use of high quality downloads (only with premium accounts)
|
||||
| ANTI_BAN_WAIT_TIME | --anti-ban-wait-time | The wait time between bulk downloads
|
||||
| OVERRIDE_AUTO_WAIT | --override-auto-wait | Totally disable wait time between songs with the risk of instability
|
||||
| CHUNK_SIZE | --chunk-size | chunk size for downloading
|
||||
| SPLIT_ALBUM_DISCS | --split-album-discs | split downloaded albums by disc
|
||||
| DOWNLOAD_REAL_TIME | --download-real-time | only downloads songs as fast as they would be played, can prevent account bans
|
||||
| LANGUAGE | --language | Language for spotify metadata
|
||||
| BITRATE | --bitrate | Overwrite the bitrate for ffmpeg encoding
|
||||
| SONG_ARCHIVE | --song-archive | The song_archive file for SKIP_PREVIOUSLY_DOWNLOADED
|
||||
| CREDENTIALS_LOCATION | --credentials-location | The location of the credentials.json
|
||||
| OUTPUT | --output | The output location/format (see below)
|
||||
| PRINT_SPLASH | --print-splash | Print the splash message
|
||||
| PRINT_SKIPS | --print-skips | Print messages if a song is being skipped
|
||||
| PRINT_DOWNLOAD_PROGRESS | --print-download-progress | Print the download/playlist progress bars
|
||||
| PRINT_ERRORS | --print-errors | Print errors
|
||||
| PRINT_DOWNLOADS | --print-downloads | Print messages when a song is finished downloading
|
||||
| TEMP_DOWNLOAD_DIR | --temp-download-dir | Download tracks to a temporary directory first
|
||||
|
||||
### Output format:
|
||||
|
||||
With the option `OUTPUT` (or the commandline parameter `--output`) you can specify the output location and format.
|
||||
The value is relative to the `ROOT_PATH`/`ROOT_PODCAST_PATH` directory and can contain the following placeholder:
|
||||
|
||||
| Placeholder | Description
|
||||
|-----------------|--------------------------------
|
||||
| {artist} | The song artist
|
||||
| {album} | The song album
|
||||
| {song_name} | The song name
|
||||
| {release_year} | The song release year
|
||||
| {disc_number} | The disc number
|
||||
| {track_number} | The track_number
|
||||
| {id} | The song id
|
||||
| {track_id} | The track id
|
||||
| {ext} | The file extension
|
||||
| {album_id} | (only when downloading albums) ID of the album
|
||||
| {album_num} | (only when downloading albums) Incrementing track number
|
||||
| {playlist} | (only when downloading playlists) Name of the playlist
|
||||
| {playlist_num} | (only when downloading playlists) Incrementing track number
|
||||
|
||||
Example values could be:
|
||||
~~~~
|
||||
{playlist}/{artist} - {song_name}.{ext}
|
||||
{playlist}/{playlist_num} - {artist} - {song_name}.{ext}
|
||||
Liked Songs/{artist} - {song_name}.{ext}
|
||||
{artist} - {song_name}.{ext}
|
||||
{artist}/{album}/{album_num} - {artist} - {song_name}.{ext}
|
||||
/home/user/downloads/{artist} - {song_name} [{id}].{ext}
|
||||
~~~~
|
||||
|
||||
### Docker Usage
|
||||
|
||||
```
|
||||
|
@ -3,6 +3,5 @@ git+https://github.com/kokarare1212/librespot-python
|
||||
music_tag
|
||||
Pillow
|
||||
protobuf
|
||||
pydub
|
||||
tabulate
|
||||
tqdm
|
@ -26,18 +26,18 @@ def get_album_tracks(album_id):
|
||||
|
||||
def get_album_name(album_id):
|
||||
""" Returns album name """
|
||||
resp = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}')
|
||||
(raw, resp) = ZSpotify.invoke_url(f'{ALBUM_URL}/{album_id}')
|
||||
return resp[ARTISTS][0][NAME], fix_filename(resp[NAME])
|
||||
|
||||
|
||||
def get_artist_albums(artist_id):
|
||||
""" Returns artist's albums """
|
||||
resp = ZSpotify.invoke_url(f'{ARTIST_URL}/{artist_id}/albums?include_groups=album%2Csingle')
|
||||
(raw, 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
|
||||
while resp['next'] is not None:
|
||||
resp = ZSpotify.invoke_url(resp['next'])
|
||||
(raw, resp) = ZSpotify.invoke_url(resp['next'])
|
||||
album_ids.extend([resp[ITEMS][i][ID] for i in range(len(resp[ITEMS]))])
|
||||
|
||||
return album_ids
|
||||
|
@ -9,7 +9,7 @@ from playlist import get_playlist_songs, get_playlist_info, download_from_user_p
|
||||
from podcast import download_episode, get_show_episodes
|
||||
from termoutput import Printer, PrintChannel
|
||||
from track import download_track, get_saved_tracks
|
||||
from utils import fix_filename, splash, split_input, regex_input_for_urls
|
||||
from utils import splash, split_input, regex_input_for_urls
|
||||
from zspotify import ZSpotify
|
||||
|
||||
SEARCH_URL = 'https://api.spotify.com/v1/search'
|
||||
@ -49,7 +49,7 @@ def client(args) -> None:
|
||||
if args.liked_songs:
|
||||
for song in get_saved_tracks():
|
||||
if not song[TRACK][NAME]:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###' + "\n")
|
||||
Printer.print(PrintChannel.SKIPS, '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###' + "\n")
|
||||
else:
|
||||
download_track('liked', song[TRACK][ID])
|
||||
|
||||
@ -85,8 +85,11 @@ def download_from_urls(urls: list[str]) -> bool:
|
||||
enum = 1
|
||||
char_num = len(str(len(playlist_songs)))
|
||||
for song in playlist_songs:
|
||||
download_track('playlist', song[TRACK][ID], extra_keys={'playlist': name, 'playlist_num': str(enum).zfill(char_num)})
|
||||
enum += 1
|
||||
if not song[TRACK][NAME]:
|
||||
Printer.print(PrintChannel.SKIPS, '### SKIPPING: SONG DOES NOT EXIST ON SPOTIFY ANYMORE ###' + "\n")
|
||||
else:
|
||||
download_track('playlist', song[TRACK][ID], extra_keys={'playlist': name, 'playlist_num': str(enum).zfill(char_num)})
|
||||
enum += 1
|
||||
elif episode_id is not None:
|
||||
download = True
|
||||
download_episode(episode_id)
|
||||
@ -256,11 +259,9 @@ def search(search_term):
|
||||
print('NO RESULTS FOUND - EXITING...')
|
||||
else:
|
||||
selection = ''
|
||||
print('\n> SELECT A DOWNLOAD OPTION BY ID')
|
||||
print('> SELECT A DOWNLOAD OPTION BY ID')
|
||||
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
||||
print('> For example, typing 5 to get option 5 or 10-20 to get\nevery option from 10-20 (inclusive)\n')
|
||||
print('> Or type 10,12,15,18 to get those options in particular')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||
while len(selection) == 0:
|
||||
selection = str(input('ID(s): '))
|
||||
inputs = split_input(selection)
|
||||
|
@ -1,8 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
CONFIG_FILE_PATH = '../zs_config.json'
|
||||
|
||||
@ -27,6 +25,7 @@ PRINT_SKIPS = 'PRINT_SKIPS'
|
||||
PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS'
|
||||
PRINT_ERRORS = 'PRINT_ERRORS'
|
||||
PRINT_DOWNLOADS = 'PRINT_DOWNLOADS'
|
||||
TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR'
|
||||
|
||||
CONFIG_VALUES = {
|
||||
ROOT_PATH: { 'default': '../ZSpotify Music/', 'type': str, 'arg': '--root-path' },
|
||||
@ -50,6 +49,7 @@ CONFIG_VALUES = {
|
||||
PRINT_DOWNLOAD_PROGRESS: { 'default': 'True', 'type': bool, 'arg': '--print-download-progress' },
|
||||
PRINT_ERRORS: { 'default': 'True', 'type': bool, 'arg': '--print-errors' },
|
||||
PRINT_DOWNLOADS: { 'default': 'False', 'type': bool, 'arg': '--print-downloads' },
|
||||
TEMP_DOWNLOAD_DIR: { 'default': '', 'type': str, 'arg': '--temp-download-dir' },
|
||||
}
|
||||
|
||||
OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}'
|
||||
@ -129,11 +129,11 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def get_root_path(cls) -> str:
|
||||
return cls.get(ROOT_PATH)
|
||||
return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PATH))
|
||||
|
||||
@classmethod
|
||||
def get_root_podcast_path(cls) -> str:
|
||||
return cls.get(ROOT_PODCAST_PATH)
|
||||
return os.path.join(os.path.dirname(__file__), cls.get(ROOT_PODCAST_PATH))
|
||||
|
||||
@classmethod
|
||||
def get_skip_existing_files(cls) -> bool:
|
||||
@ -181,11 +181,17 @@ class Config:
|
||||
|
||||
@classmethod
|
||||
def get_song_archive(cls) -> str:
|
||||
return cls.get(SONG_ARCHIVE)
|
||||
return os.path.join(cls.get_root_path(), cls.get(SONG_ARCHIVE))
|
||||
|
||||
@classmethod
|
||||
def get_credentials_location(cls) -> str:
|
||||
return cls.get(CREDENTIALS_LOCATION)
|
||||
return os.path.join(os.getcwd(), cls.get(CREDENTIALS_LOCATION))
|
||||
|
||||
@classmethod
|
||||
def get_temp_download_dir(cls) -> str:
|
||||
if cls.get(TEMP_DOWNLOAD_DIR) == '':
|
||||
return ''
|
||||
return os.path.join(cls.get_root_path(), cls.get(TEMP_DOWNLOAD_DIR))
|
||||
|
||||
@classmethod
|
||||
def get_output(cls, mode: str) -> str:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from const import ITEMS, ID, TRACK, NAME
|
||||
from termoutput import Printer
|
||||
from track import download_track
|
||||
from utils import fix_filename, split_input
|
||||
from utils import split_input
|
||||
from zspotify import ZSpotify
|
||||
|
||||
MY_PLAYLISTS_URL = 'https://api.spotify.com/v1/me/playlists'
|
||||
@ -42,7 +42,7 @@ def get_playlist_songs(playlist_id):
|
||||
|
||||
def get_playlist_info(playlist_id):
|
||||
""" Returns information scraped from playlist """
|
||||
resp = ZSpotify.invoke_url(f'{PLAYLISTS_URL}/{playlist_id}?fields=name,owner(display_name)&market=from_token')
|
||||
(raw, resp) = ZSpotify.invoke_url(f'{PLAYLISTS_URL}/{playlist_id}?fields=name,owner(display_name)&market=from_token')
|
||||
return resp['name'].strip(), resp['owner']['display_name'].strip()
|
||||
|
||||
|
||||
@ -70,9 +70,7 @@ def download_from_user_playlist():
|
||||
selection = ''
|
||||
print('\n> SELECT A PLAYLIST BY ID')
|
||||
print('> SELECT A RANGE BY ADDING A DASH BETWEEN BOTH ID\'s')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s')
|
||||
print('> For example, typing 10 to get one playlist or 10-20 to get\nevery playlist from 10-20 (inclusive)\n')
|
||||
print('> Or type 10,12,15,18 to get those playlists in particular')
|
||||
print('> OR PARTICULAR OPTIONS BY ADDING A COMMA BETWEEN ID\'s\n')
|
||||
while len(selection) == 0:
|
||||
selection = str(input('ID(s): '))
|
||||
playlist_choices = map(int, split_input(selection))
|
||||
|
@ -1,7 +1,6 @@
|
||||
import os
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from librespot.audio.decoders import VorbisOnlyAudioQuality
|
||||
from librespot.metadata import EpisodeId
|
||||
|
||||
from const import (ERROR, ID, ITEMS, NAME, SHOW)
|
||||
@ -14,7 +13,7 @@ SHOWS_URL = 'https://api.spotify.com/v1/shows'
|
||||
|
||||
|
||||
def get_episode_info(episode_id_str) -> Tuple[Optional[str], Optional[str]]:
|
||||
info = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
|
||||
(raw, info) = ZSpotify.invoke_url(f'{EPISODE_INFO_URL}/{episode_id_str}')
|
||||
if ERROR in info:
|
||||
return None, None
|
||||
return fix_filename(info[SHOW][NAME]), fix_filename(info[NAME])
|
||||
@ -70,18 +69,14 @@ def download_episode(episode_id) -> None:
|
||||
extra_paths = podcast_name + '/'
|
||||
|
||||
if podcast_name is None:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: (EPISODE NOT FOUND) ###')
|
||||
Printer.print(PrintChannel.SKIPS, '### SKIPPING: (EPISODE NOT FOUND) ###')
|
||||
else:
|
||||
filename = podcast_name + ' - ' + episode_name
|
||||
|
||||
direct_download_url = ZSpotify.invoke_url(
|
||||
'https://api-partner.spotify.com/pathfinder/v1/query?operationName=getEpisode&variables={"uri":"spotify:episode:' + episode_id + '"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"224ba0fd89fcfdfb3a15fa2d82a6112d3f4e2ac88fba5c6713de04d1b72cf482"}}')["data"]["episode"]["audio"]["items"][-1]["url"]
|
||||
'https://api-partner.spotify.com/pathfinder/v1/query?operationName=getEpisode&variables={"uri":"spotify:episode:' + episode_id + '"}&extensions={"persistedQuery":{"version":1,"sha256Hash":"224ba0fd89fcfdfb3a15fa2d82a6112d3f4e2ac88fba5c6713de04d1b72cf482"}}')[1]["data"]["episode"]["audio"]["items"][-1]["url"]
|
||||
|
||||
download_directory = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
ZSpotify.CONFIG.get_root_podcast_path(),
|
||||
extra_paths,
|
||||
)
|
||||
download_directory = os.path.join(ZSpotify.CONFIG.get_root_podcast_path(), extra_paths)
|
||||
download_directory = os.path.realpath(download_directory)
|
||||
create_download_directory(download_directory)
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
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 const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \
|
||||
RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS
|
||||
from termoutput import Printer, PrintChannel
|
||||
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
|
||||
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive
|
||||
get_directory_song_ids, add_to_directory_song_ids, get_previously_downloaded, add_to_archive, fmt_seconds
|
||||
from zspotify import ZSpotify
|
||||
|
||||
import traceback
|
||||
|
||||
def get_saved_tracks() -> list:
|
||||
""" Returns user's saved tracks """
|
||||
@ -35,27 +35,34 @@ def get_saved_tracks() -> list:
|
||||
|
||||
def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any, int]:
|
||||
""" Retrieves metadata for downloaded songs """
|
||||
info = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token')
|
||||
(raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token')
|
||||
|
||||
artists = []
|
||||
for data in info[TRACKS][0][ARTISTS]:
|
||||
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]
|
||||
track_number = info[TRACKS][0][TRACK_NUMBER]
|
||||
scraped_song_id = info[TRACKS][0][ID]
|
||||
is_playable = info[TRACKS][0][IS_PLAYABLE]
|
||||
duration_ms = info[TRACKS][0][DURATION_MS]
|
||||
if not TRACKS in info:
|
||||
raise ValueError(f'Invalid response from TRACKS_URL:\n{raw}')
|
||||
|
||||
try:
|
||||
artists = []
|
||||
for data in info[TRACKS][0][ARTISTS]:
|
||||
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]
|
||||
track_number = info[TRACKS][0][TRACK_NUMBER]
|
||||
scraped_song_id = info[TRACKS][0][ID]
|
||||
is_playable = info[TRACKS][0][IS_PLAYABLE]
|
||||
duration_ms = info[TRACKS][0][DURATION_MS]
|
||||
|
||||
return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms
|
||||
except Exception as e:
|
||||
raise ValueError(f'Failed to parse TRACKS_URL response: {str(e)}\n{raw}')
|
||||
|
||||
return artists, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms
|
||||
|
||||
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}')
|
||||
(raw, resp) = ZSpotify.invoke_url(f'{TRACK_STATS_URL}{song_id}')
|
||||
|
||||
# get duration in miliseconds
|
||||
ms_duration = resp['duration_ms']
|
||||
@ -83,6 +90,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
for k in extra_keys:
|
||||
output_template = output_template.replace("{"+k+"}", fix_filename(extra_keys[k]))
|
||||
|
||||
ext = EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower())
|
||||
|
||||
output_template = output_template.replace("{artist}", fix_filename(artists[0]))
|
||||
output_template = output_template.replace("{album}", fix_filename(album_name))
|
||||
output_template = output_template.replace("{song_name}", fix_filename(name))
|
||||
@ -91,11 +100,15 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
output_template = output_template.replace("{track_number}", fix_filename(track_number))
|
||||
output_template = output_template.replace("{id}", fix_filename(scraped_song_id))
|
||||
output_template = output_template.replace("{track_id}", fix_filename(track_id))
|
||||
output_template = output_template.replace("{ext}", EXT_MAP.get(ZSpotify.CONFIG.get_download_format().lower()))
|
||||
output_template = output_template.replace("{ext}", ext)
|
||||
|
||||
filename = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), output_template)
|
||||
filename = os.path.join(ZSpotify.CONFIG.get_root_path(), output_template)
|
||||
filedir = os.path.dirname(filename)
|
||||
|
||||
filename_temp = filename
|
||||
if ZSpotify.CONFIG.get_temp_download_dir() != '':
|
||||
filename_temp = os.path.join(ZSpotify.CONFIG.get_temp_download_dir(), f'zspotify_{str(uuid.uuid4())}_{track_id}.{ext}')
|
||||
|
||||
check_name = os.path.isfile(filename) and os.path.getsize(filename)
|
||||
check_id = scraped_song_id in get_directory_song_ids(filedir)
|
||||
check_all_time = scraped_song_id in get_previously_downloaded()
|
||||
@ -112,7 +125,9 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
|
||||
except Exception as e:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING SONG - FAILED TO QUERY METADATA ###')
|
||||
Printer.print(PrintChannel.ERRORS, 'Track_ID: ' + str(track_id) + "\n")
|
||||
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
|
||||
Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
|
||||
else:
|
||||
try:
|
||||
if not is_playable:
|
||||
@ -128,12 +143,13 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
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)
|
||||
stream = ZSpotify.get_content_stream(track_id, ZSpotify.DOWNLOAD_QUALITY)
|
||||
create_download_directory(filedir)
|
||||
total_size = stream.input_stream.size
|
||||
|
||||
with open(filename, 'wb') as file, Printer.progress(
|
||||
time_start = time.time()
|
||||
downloaded = 0
|
||||
with open(filename_temp, 'wb') as file, Printer.progress(
|
||||
desc=song_name,
|
||||
total=total_size,
|
||||
unit='B',
|
||||
@ -141,18 +157,28 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
unit_divisor=1024,
|
||||
disable=disable_progressbar
|
||||
) as p_bar:
|
||||
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())
|
||||
p_bar.update(file.write(data))
|
||||
downloaded += len(data)
|
||||
if ZSpotify.CONFIG.get_download_real_time():
|
||||
time.sleep(pause)
|
||||
delta_real = time.time() - time_start
|
||||
delta_want = (downloaded / total_size) * (duration_ms/1000)
|
||||
if delta_want > delta_real:
|
||||
time.sleep(delta_want - delta_real)
|
||||
|
||||
convert_audio_format(filename)
|
||||
set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number)
|
||||
set_music_thumbnail(filename, image_url)
|
||||
time_downloaded = time.time()
|
||||
|
||||
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, os.path.dirname(__file__))}" ###' + "\n")
|
||||
convert_audio_format(filename_temp)
|
||||
set_audio_tags(filename_temp, artists, name, album_name, release_year, disc_number, track_number)
|
||||
set_music_thumbnail(filename_temp, image_url)
|
||||
|
||||
if filename_temp != filename:
|
||||
os.rename(filename_temp, filename)
|
||||
|
||||
time_finished = time.time()
|
||||
|
||||
Printer.print(PrintChannel.DOWNLOADS, f'### Downloaded "{song_name}" to "{os.path.relpath(filename, ZSpotify.CONFIG.get_root_path())}" in {fmt_seconds(time_downloaded - time_start)} (plus {fmt_seconds(time_finished - time_downloaded)} converting) ###' + "\n")
|
||||
|
||||
# add song id to archive file
|
||||
if ZSpotify.CONFIG.get_skip_previously_downloaded():
|
||||
@ -165,9 +191,11 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
|
||||
time.sleep(ZSpotify.CONFIG.get_anti_ban_wait_time())
|
||||
except Exception as e:
|
||||
Printer.print(PrintChannel.ERRORS, '### SKIPPING: ' + song_name + ' (GENERAL DOWNLOAD ERROR) ###')
|
||||
Printer.print(PrintChannel.ERRORS, 'Track_ID: ' + str(track_id) + "\n")
|
||||
Printer.print(PrintChannel.ERRORS, str(e) + "\n")
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
|
||||
if os.path.exists(filename_temp):
|
||||
os.remove(filename_temp)
|
||||
|
||||
|
||||
def convert_audio_format(filename) -> None:
|
||||
|
@ -1,9 +1,9 @@
|
||||
import datetime
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import List, Tuple
|
||||
|
||||
@ -30,11 +30,12 @@ def create_download_directory(download_path: str) -> None:
|
||||
with open(hidden_file_path, 'w', encoding='utf-8') as f:
|
||||
pass
|
||||
|
||||
|
||||
def get_previously_downloaded() -> List[str]:
|
||||
""" Returns list of all time downloaded songs """
|
||||
|
||||
ids = []
|
||||
archive_path = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), ZSpotify.CONFIG.get_song_archive())
|
||||
archive_path = ZSpotify.CONFIG.get_song_archive()
|
||||
|
||||
if os.path.exists(archive_path):
|
||||
with open(archive_path, 'r', encoding='utf-8') as f:
|
||||
@ -42,10 +43,11 @@ def get_previously_downloaded() -> List[str]:
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
||||
""" Adds song id to all time installed songs archive """
|
||||
|
||||
archive_path = os.path.join(os.path.dirname(__file__), ZSpotify.CONFIG.get_root_path(), ZSpotify.CONFIG.get_song_archive())
|
||||
archive_path = ZSpotify.CONFIG.get_song_archive()
|
||||
|
||||
if os.path.exists(archive_path):
|
||||
with open(archive_path, 'a', encoding='utf-8') as file:
|
||||
@ -54,6 +56,7 @@ def add_to_archive(song_id: str, filename: str, author_name: str, song_name: str
|
||||
with open(archive_path, 'w', encoding='utf-8') as file:
|
||||
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
||||
|
||||
|
||||
def get_directory_song_ids(download_path: str) -> List[str]:
|
||||
""" Gets song ids of songs in directory """
|
||||
|
||||
@ -66,6 +69,7 @@ def get_directory_song_ids(download_path: str) -> List[str]:
|
||||
|
||||
return song_ids
|
||||
|
||||
|
||||
def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, author_name: str, song_name: str) -> None:
|
||||
""" Appends song_id to .song_ids file in directory """
|
||||
|
||||
@ -75,6 +79,7 @@ def add_to_directory_song_ids(download_path: str, song_id: str, filename: str, a
|
||||
with open(hidden_file_path, 'a', encoding='utf-8') as file:
|
||||
file.write(f'{song_id}\t{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\t{author_name}\t{song_name}\t{filename}\n')
|
||||
|
||||
|
||||
def get_downloaded_song_duration(filename: str) -> float:
|
||||
""" Returns the downloaded file's duration in seconds """
|
||||
|
||||
@ -251,3 +256,26 @@ def fix_filename(name):
|
||||
True
|
||||
"""
|
||||
return re.sub(r'[/\\:|<>"?*\0-\x1f]|^(AUX|COM[1-9]|CON|LPT[1-9]|NUL|PRN)(?![^.])|^\s|[\s.]$', "_", str(name), flags=re.IGNORECASE)
|
||||
|
||||
|
||||
def fmt_seconds(secs: float) -> str:
|
||||
val = math.floor(secs)
|
||||
|
||||
s = math.floor(val % 60)
|
||||
val -= s
|
||||
val /= 60
|
||||
|
||||
m = math.floor(val % 60)
|
||||
val -= m
|
||||
val /= 60
|
||||
|
||||
h = math.floor(val)
|
||||
|
||||
if h == 0 and m == 0 and s == 0:
|
||||
return "0"
|
||||
elif h == 0 and m == 0:
|
||||
return f'{s}'.zfill(2)
|
||||
elif h == 0:
|
||||
return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||
else:
|
||||
return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
|
||||
|
@ -6,18 +6,16 @@ It's like youtube-dl, but for Spotify.
|
||||
|
||||
(Made by Deathmonger/Footsiefat - @doomslayer117:matrix.org)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
from getpass import getpass
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from librespot.audio.decoders import VorbisOnlyAudioQuality
|
||||
from librespot.core import Session
|
||||
|
||||
from const import TYPE, \
|
||||
PREMIUM, USER_READ_EMAIL, AUTHORIZATION, OFFSET, LIMIT, \
|
||||
PREMIUM, USER_READ_EMAIL, OFFSET, LIMIT, \
|
||||
PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ
|
||||
from config import Config
|
||||
|
||||
@ -35,7 +33,7 @@ class ZSpotify:
|
||||
def login(cls):
|
||||
""" Authenticates with Spotify and saves credentials to a file """
|
||||
|
||||
cred_location = os.path.join(os.getcwd(), Config.get_credentials_location())
|
||||
cred_location = Config.get_credentials_location()
|
||||
|
||||
if os.path.isfile(cred_location):
|
||||
try:
|
||||
@ -49,7 +47,8 @@ class ZSpotify:
|
||||
user_name = input('Username: ')
|
||||
password = getpass()
|
||||
try:
|
||||
cls.SESSION = Session.Builder().user_pass(user_name, password).create()
|
||||
conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build()
|
||||
cls.SESSION = Session.Builder(conf).user_pass(user_name, password).create()
|
||||
return
|
||||
except RuntimeError:
|
||||
pass
|
||||
@ -85,7 +84,8 @@ class ZSpotify:
|
||||
@classmethod
|
||||
def invoke_url(cls, url):
|
||||
headers = cls.get_auth_header()
|
||||
return requests.get(url, headers=headers).json()
|
||||
response = requests.get(url, headers=headers)
|
||||
return response.text, response.json()
|
||||
|
||||
@classmethod
|
||||
def check_premium(cls) -> bool:
|
||||
|
Loading…
Reference in New Issue
Block a user