mirror of
				https://github.com/THIS-IS-NOT-A-BACKUP/zspotify.git
				synced 2025-11-03 21:10:34 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/main'
This commit is contained in:
		
						commit
						9f99b893cd
					
				
							
								
								
									
										60
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@ -1,11 +1,37 @@
 | 
				
			|||||||
## **Changelog:**
 | 
					# Changelog:
 | 
				
			||||||
**v2.4 (27 Oct 20212):**
 | 
					### v0.5.2 - We're bad at counting (27 Nov 2021):
 | 
				
			||||||
 | 
					**General changes:**
 | 
				
			||||||
 | 
					- Fixed filenaming on Windows
 | 
				
			||||||
 | 
					- Fixed removal of special characters metadata
 | 
				
			||||||
 | 
					- Can now download different songs with the same name
 | 
				
			||||||
 | 
					- Real-time downloads now work correctly
 | 
				
			||||||
 | 
					- Removed some debug messages
 | 
				
			||||||
 | 
					- Added album_artist metadata
 | 
				
			||||||
 | 
					- Added global song archive
 | 
				
			||||||
 | 
					- Added SONG_ARCHIVE config value
 | 
				
			||||||
 | 
					- Added CREDENTIALS_LOCATION config value
 | 
				
			||||||
 | 
					- Added `--download` argument
 | 
				
			||||||
 | 
					- Added `--config-location` argument
 | 
				
			||||||
 | 
					- Added `--output` for output templating
 | 
				
			||||||
 | 
					- Save extra data in .song_ids
 | 
				
			||||||
 | 
					- Added options to regulate terminal output
 | 
				
			||||||
 | 
					- Direct download support for certain podcasts  
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					**Docker images:**
 | 
				
			||||||
 | 
					- Remember credentials between container starts
 | 
				
			||||||
 | 
					- Use same uid/gid in container as on host  
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					**Windows installer:**
 | 
				
			||||||
 | 
					- Now comes with full installer
 | 
				
			||||||
 | 
					- Dependencies are installed if not found
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### v0.2.4 (27 Oct 2021):
 | 
				
			||||||
- Added realtime downloading support to avoid account suspensions.
 | 
					- Added realtime downloading support to avoid account suspensions.
 | 
				
			||||||
- Fix for downloading by artist.
 | 
					- Fix for downloading by artist.
 | 
				
			||||||
- Replace audio conversion method for better quality.
 | 
					- Replace audio conversion method for better quality.
 | 
				
			||||||
- Fix bug when automatically setting audio bitrate.
 | 
					- Fix bug when automatically setting audio bitrate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v2.3 (25 Oct 2021):**
 | 
					### v0.2.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).
 | 
				
			||||||
- Fixed spelling errors.
 | 
					- Fixed spelling errors.
 | 
				
			||||||
@ -17,7 +43,7 @@
 | 
				
			|||||||
- Fixed issue where if you enabled splitting discs into seperate folders downloading would fail.
 | 
					- Fixed issue where if you enabled splitting discs into seperate folders downloading would fail.
 | 
				
			||||||
- Added playlist file(m3u) creation for playlist download.
 | 
					- Added playlist file(m3u) creation for playlist download.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v2.2 (24 Oct 2021):**
 | 
					### v0.2.2 (24 Oct 2021):
 | 
				
			||||||
- Added basic support for downloading an entire podcast series.
 | 
					- Added basic support for downloading an entire podcast series.
 | 
				
			||||||
- Split code into multiple files for easier maintenance.
 | 
					- Split code into multiple files for easier maintenance.
 | 
				
			||||||
- Changed initial launch script to app.py
 | 
					- Changed initial launch script to app.py
 | 
				
			||||||
@ -28,39 +54,39 @@
 | 
				
			|||||||
- Fixed artist names getting cutoff in metadata.
 | 
					- Fixed artist names getting cutoff in metadata.
 | 
				
			||||||
- Removed data sanitization of metadata tags. 
 | 
					- Removed data sanitization of metadata tags. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v2.1 (23 Oct 2021):**
 | 
					### v0.2.1 (23 Oct 2021):
 | 
				
			||||||
- Moved configuration from hard-coded values to separate zs_config.json file.
 | 
					- Moved configuration from hard-coded values to separate zs_config.json file.
 | 
				
			||||||
- Add subfolders for each disc.
 | 
					- Add subfolders for each disc.
 | 
				
			||||||
- Can now search and download all songs by artist.
 | 
					- Can now search and download all songs by artist.
 | 
				
			||||||
- Show single progress bar for entire album.
 | 
					- Show single progress bar for entire album.
 | 
				
			||||||
- Added song number at start of track name in albums.
 | 
					- Added song number at start of track name in albums.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v2.0 (22 Oct 2021):**
 | 
					### v0.2.0 (22 Oct 2021):
 | 
				
			||||||
- Added progress bar for downloads.
 | 
					- Added progress bar for downloads.
 | 
				
			||||||
- Added multi-select support for all results when searching.
 | 
					- Added multi-select support for all results when searching.
 | 
				
			||||||
- Added GPLv3 Licence.
 | 
					- Added GPLv3 Licence.
 | 
				
			||||||
- Changed welcome banner and removed unnecessary debug print statements.
 | 
					- Changed welcome banner and removed unnecessary debug print statements.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.9 (22 Oct 2021):**
 | 
					### v0.1.9 (22 Oct 2021):
 | 
				
			||||||
- Added Gitea mirror for when the Spotify Glowies come to DMCA the shit out of this.
 | 
					- Added Gitea mirror for when the Spotify Glowies come to DMCA the shit out of this.
 | 
				
			||||||
- Changed the discord server invite to a matrix server so that won't get swatted either.
 | 
					- Changed the discord server invite to a matrix server so that won't get swatted either.
 | 
				
			||||||
- Added option to select multiple of our saved playlists to download at once.
 | 
					- Added option to select multiple of our saved playlists to download at once.
 | 
				
			||||||
- Added support for downloading an entire show at once.
 | 
					- Added support for downloading an entire show at once.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.8 (21 Oct 2021):**
 | 
					### v0.1.8 (21 Oct 2021):
 | 
				
			||||||
- Improved podcast downloading a bit.
 | 
					- Improved podcast downloading a bit.
 | 
				
			||||||
- Simplified the code that catches crashes while downloading.
 | 
					- Simplified the code that catches crashes while downloading.
 | 
				
			||||||
- Cleaned up code using linter again.
 | 
					- Cleaned up code using linter again.
 | 
				
			||||||
- Added option to just paste a url in the search bar to download it.
 | 
					- Added option to just paste a url in the search bar to download it.
 | 
				
			||||||
- Added a small delay between downloading each track when downloading in bulk to help with downloading issues and potential bans.
 | 
					- Added a small delay between downloading each track when downloading in bulk to help with downloading issues and potential bans.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.7 (21 Oct 2021):**
 | 
					### v0.1.7 (21 Oct 2021):
 | 
				
			||||||
- Rewrote README.md to look a lot more professional.
 | 
					- Rewrote README.md to look a lot more professional.
 | 
				
			||||||
- Added patch to fix edge case crash when downloading liked songs.
 | 
					- Added patch to fix edge case crash when downloading liked songs.
 | 
				
			||||||
- Made premium account check a lot more reliable.
 | 
					- Made premium account check a lot more reliable.
 | 
				
			||||||
- Added experimental podcast support for specific episodes!
 | 
					- Added experimental podcast support for specific episodes!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.6 (20 Oct 2021):**
 | 
					### v0.1.6 (20 Oct 2021):
 | 
				
			||||||
- Added Pillow to requirements.txt.
 | 
					- Added Pillow to requirements.txt.
 | 
				
			||||||
- Removed websocket-client from requirements.txt because librespot-python added it to their dependency list.
 | 
					- Removed websocket-client from requirements.txt because librespot-python added it to their dependency list.
 | 
				
			||||||
- Made it hide your password when you type it in.
 | 
					- Made it hide your password when you type it in.
 | 
				
			||||||
@ -69,34 +95,34 @@
 | 
				
			|||||||
- Added Shebang line so it runs smoother on Linux.
 | 
					- Added Shebang line so it runs smoother on Linux.
 | 
				
			||||||
- Made it download the entire track at once now so it is more efficient and fixed a bug users encountered.
 | 
					- Made it download the entire track at once now so it is more efficient and fixed a bug users encountered.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.5 (19 Oct 2021):**
 | 
					### v0.1.5 (19 Oct 2021):
 | 
				
			||||||
- Made downloading a lot more efficient and probably faster.
 | 
					- Made downloading a lot more efficient and probably faster.
 | 
				
			||||||
- Made the sanitizer more efficient.
 | 
					- Made the sanitizer more efficient.
 | 
				
			||||||
- Formatted and linted all the code.
 | 
					- Formatted and linted all the code.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.4 (19 Oct 2021):**
 | 
					### v0.1.4 (19 Oct 2021):
 | 
				
			||||||
- Added option to encode the downloaded tracks in the "ogg" format rather than "mp3".
 | 
					- Added option to encode the downloaded tracks in the "ogg" format rather than "mp3".
 | 
				
			||||||
- Added small improvement to sanitation function so it catches another edge case.
 | 
					- Added small improvement to sanitation function so it catches another edge case.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.3 (19 Oct 2021):**
 | 
					### v0.1.3 (19 Oct 2021):
 | 
				
			||||||
- Added auto detection about if the current account is premium or not. If it is a premium account it automatically sets the quality to VERY_HIGH and otherwise HIGH if we are using a free account.
 | 
					- Added auto detection about if the current account is premium or not. If it is a premium account it automatically sets the quality to VERY_HIGH and otherwise HIGH if we are using a free account.
 | 
				
			||||||
- Fixed conversion function so it now exports to the correct bitrate.
 | 
					- Fixed conversion function so it now exports to the correct bitrate.
 | 
				
			||||||
- Added sanitation to playlist names to help catch an edge case crash.
 | 
					- Added sanitation to playlist names to help catch an edge case crash.
 | 
				
			||||||
- Added option to download all your liked songs into a sub-folder.
 | 
					- Added option to download all your liked songs into a sub-folder.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.2 (18 Oct 2021):**
 | 
					### v0.1.2 (18 Oct 2021):
 | 
				
			||||||
- Added .gitignore.
 | 
					- Added .gitignore.
 | 
				
			||||||
- Replaced dependency list in README.md with a proper requirements.txt file.
 | 
					- Replaced dependency list in README.md with a proper requirements.txt file.
 | 
				
			||||||
- Improved the readability of README.md.
 | 
					- Improved the readability of README.md.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.1 (16 Oct 2021):**
 | 
					### v0.1.1 (16 Oct 2021):
 | 
				
			||||||
- Added try/except to help catch crashes where a very few specific tracks would crash either the downloading or conversion part.
 | 
					- Added try/except to help catch crashes where a very few specific tracks would crash either the downloading or conversion part.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v1.0 (14 Oct 2021):**
 | 
					### v0.1.0 (14 Oct 2021):
 | 
				
			||||||
- Adjusted some functions so it runs again with the newer version of librespot-python.
 | 
					- Adjusted some functions so it runs again with the newer version of librespot-python.
 | 
				
			||||||
- Improved my sanitization function so it catches more edge cases.
 | 
					- Improved my sanitization function so it catches more edge cases.
 | 
				
			||||||
- Fixed an issue where sometimes spotify wouldn't provide a song id for a track we are trying to download. It will now detect and skip these invalid tracks.
 | 
					- Fixed an issue where sometimes spotify wouldn't provide a song id for a track we are trying to download. It will now detect and skip these invalid tracks.
 | 
				
			||||||
- Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped.
 | 
					- Added additional check for tracks that cannot be "played" due to licence(and similar) issues. These tracks will be skipped.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**v0.9 (13 Oct 2021):**
 | 
					### v0.0.9 (13 Oct 2021):
 | 
				
			||||||
- Initial upload, needs adjustments to get working again after backend rewrite.
 | 
					- Initial upload, needs adjustments to get working again after backend rewrite.
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,10 @@ PRINT_SKIPS = 'PRINT_SKIPS'
 | 
				
			|||||||
PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS'
 | 
					PRINT_DOWNLOAD_PROGRESS = 'PRINT_DOWNLOAD_PROGRESS'
 | 
				
			||||||
PRINT_ERRORS = 'PRINT_ERRORS'
 | 
					PRINT_ERRORS = 'PRINT_ERRORS'
 | 
				
			||||||
PRINT_DOWNLOADS = 'PRINT_DOWNLOADS'
 | 
					PRINT_DOWNLOADS = 'PRINT_DOWNLOADS'
 | 
				
			||||||
 | 
					PRINT_API_ERRORS = 'PRINT_API_ERRORS'
 | 
				
			||||||
TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR'
 | 
					TEMP_DOWNLOAD_DIR = 'TEMP_DOWNLOAD_DIR'
 | 
				
			||||||
 | 
					MD_ALLGENRES = 'MD_ALLGENRES'
 | 
				
			||||||
 | 
					MD_GENREDELIMITER = 'MD_GENREDELIMITER'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONFIG_VALUES = {
 | 
					CONFIG_VALUES = {
 | 
				
			||||||
    ROOT_PATH:                  { 'default': '../ZSpotify Music/',    'type': str,  'arg': '--root-path'                  },
 | 
					    ROOT_PATH:                  { 'default': '../ZSpotify Music/',    'type': str,  'arg': '--root-path'                  },
 | 
				
			||||||
@ -49,7 +52,10 @@ CONFIG_VALUES = {
 | 
				
			|||||||
    PRINT_DOWNLOAD_PROGRESS:    { 'default': 'True',                  'type': bool, 'arg': '--print-download-progress'    },
 | 
					    PRINT_DOWNLOAD_PROGRESS:    { 'default': 'True',                  'type': bool, 'arg': '--print-download-progress'    },
 | 
				
			||||||
    PRINT_ERRORS:               { 'default': 'True',                  'type': bool, 'arg': '--print-errors'               },
 | 
					    PRINT_ERRORS:               { 'default': 'True',                  'type': bool, 'arg': '--print-errors'               },
 | 
				
			||||||
    PRINT_DOWNLOADS:            { 'default': 'False',                 'type': bool, 'arg': '--print-downloads'            },
 | 
					    PRINT_DOWNLOADS:            { 'default': 'False',                 'type': bool, 'arg': '--print-downloads'            },
 | 
				
			||||||
    TEMP_DOWNLOAD_DIR:          { 'default': '',                      'type': str,  'arg': '--temp-download-dir'          },
 | 
					    PRINT_API_ERRORS:           { 'default': 'False',                 'type': bool, 'arg': '--print-api-errors'            },
 | 
				
			||||||
 | 
					    MD_ALLGENRES:               { 'default': 'False',                 'type': bool, 'arg': '--md-allgenres'               },
 | 
				
			||||||
 | 
					    MD_GENREDELIMITER:          { 'default': ';',                     'type': str,  'arg': '--md-genredelimiter'          },
 | 
				
			||||||
 | 
					    TEMP_DOWNLOAD_DIR:          { 'default': '',                      'type': str,  'arg': '--temp-download-dir'          }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}'
 | 
					OUTPUT_DEFAULT_PLAYLIST = '{playlist}/{artist} - {song_name}.{ext}'
 | 
				
			||||||
@ -192,7 +198,15 @@ class Config:
 | 
				
			|||||||
        if cls.get(TEMP_DOWNLOAD_DIR) == '':
 | 
					        if cls.get(TEMP_DOWNLOAD_DIR) == '':
 | 
				
			||||||
            return ''
 | 
					            return ''
 | 
				
			||||||
        return os.path.join(cls.get_root_path(), cls.get(TEMP_DOWNLOAD_DIR))
 | 
					        return os.path.join(cls.get_root_path(), cls.get(TEMP_DOWNLOAD_DIR))
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_allGenres(cls) -> bool:
 | 
				
			||||||
 | 
					        return cls.get(MD_ALLGENRES)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def get_allGenresDelimiter(cls) -> bool:
 | 
				
			||||||
 | 
					        return cls.get(MD_GENREDELIMITER)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def get_output(cls, mode: str) -> str:
 | 
					    def get_output(cls, mode: str) -> str:
 | 
				
			||||||
        v = cls.get(OUTPUT)
 | 
					        v = cls.get(OUTPUT)
 | 
				
			||||||
 | 
				
			|||||||
@ -20,6 +20,10 @@ ARTISTS = 'artists'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ALBUMARTIST = 'albumartist'
 | 
					ALBUMARTIST = 'albumartist'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GENRES = 'genres'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GENRE = 'genre'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARTWORK = 'artwork'
 | 
					ARTWORK = 'artwork'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TRACKS = 'tracks'
 | 
					TRACKS = 'tracks'
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
from enum import Enum
 | 
					from enum import Enum
 | 
				
			||||||
from tqdm import tqdm
 | 
					from tqdm import tqdm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from config import PRINT_SPLASH, PRINT_SKIPS, PRINT_DOWNLOAD_PROGRESS, PRINT_ERRORS, PRINT_DOWNLOADS
 | 
					from config import PRINT_SPLASH, PRINT_SKIPS, PRINT_DOWNLOAD_PROGRESS, PRINT_ERRORS, PRINT_DOWNLOADS, PRINT_API_ERRORS
 | 
				
			||||||
from zspotify import ZSpotify
 | 
					from zspotify import ZSpotify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -11,6 +11,7 @@ class PrintChannel(Enum):
 | 
				
			|||||||
    DOWNLOAD_PROGRESS = PRINT_DOWNLOAD_PROGRESS
 | 
					    DOWNLOAD_PROGRESS = PRINT_DOWNLOAD_PROGRESS
 | 
				
			||||||
    ERRORS = PRINT_ERRORS
 | 
					    ERRORS = PRINT_ERRORS
 | 
				
			||||||
    DOWNLOADS = PRINT_DOWNLOADS
 | 
					    DOWNLOADS = PRINT_DOWNLOADS
 | 
				
			||||||
 | 
					    API_ERRORS = PRINT_API_ERRORS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Printer:
 | 
					class Printer:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
from typing import Any, Tuple, List
 | 
					from typing import Any, Tuple, List
 | 
				
			||||||
@ -8,7 +9,7 @@ from librespot.audio.decoders import AudioQuality
 | 
				
			|||||||
from librespot.metadata import TrackId
 | 
					from librespot.metadata import TrackId
 | 
				
			||||||
from ffmpy import FFmpeg
 | 
					from ffmpy import FFmpeg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from const import TRACKS, ALBUM, NAME, ITEMS, DISC_NUMBER, TRACK_NUMBER, IS_PLAYABLE, ARTISTS, IMAGES, URL, \
 | 
					from const import TRACKS, ALBUM, GENRES, GENRE, 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
 | 
					    RELEASE_DATE, ID, TRACKS_URL, SAVED_TRACKS_URL, TRACK_STATS_URL, CODEC_MAP, EXT_MAP, DURATION_MS
 | 
				
			||||||
from termoutput import Printer, PrintChannel
 | 
					from termoutput import Printer, PrintChannel
 | 
				
			||||||
from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
 | 
					from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_download_directory, \
 | 
				
			||||||
@ -16,6 +17,8 @@ from utils import fix_filename, set_audio_tags, set_music_thumbnail, create_down
 | 
				
			|||||||
from zspotify import ZSpotify
 | 
					from zspotify import ZSpotify
 | 
				
			||||||
import traceback
 | 
					import traceback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from utils import Loader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_saved_tracks() -> list:
 | 
					def get_saved_tracks() -> list:
 | 
				
			||||||
    """ Returns user's saved tracks """
 | 
					    """ Returns user's saved tracks """
 | 
				
			||||||
    songs = []
 | 
					    songs = []
 | 
				
			||||||
@ -33,17 +36,32 @@ def get_saved_tracks() -> list:
 | 
				
			|||||||
    return songs
 | 
					    return songs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any, Any, int]:
 | 
					def get_song_info(song_id) -> Tuple[List[str], List[str], str, str, Any, Any, Any, Any, Any, Any, int]:
 | 
				
			||||||
    """ Retrieves metadata for downloaded songs """
 | 
					    """ Retrieves metadata for downloaded songs """
 | 
				
			||||||
    (raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token')
 | 
					    with Loader("Fetching track information..."):
 | 
				
			||||||
 | 
					        (raw, info) = ZSpotify.invoke_url(f'{TRACKS_URL}?ids={song_id}&market=from_token')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if not TRACKS in info:
 | 
					    if not TRACKS in info:
 | 
				
			||||||
        raise ValueError(f'Invalid response from TRACKS_URL:\n{raw}')
 | 
					        raise ValueError(f'Invalid response from TRACKS_URL:\n{raw}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        artists = []
 | 
					        artists = []
 | 
				
			||||||
 | 
					        genres = []
 | 
				
			||||||
        for data in info[TRACKS][0][ARTISTS]:
 | 
					        for data in info[TRACKS][0][ARTISTS]:
 | 
				
			||||||
            artists.append(data[NAME])
 | 
					            artists.append(data[NAME])
 | 
				
			||||||
 | 
					            # query artist genres via href, which will be the api url
 | 
				
			||||||
 | 
					            with Loader("Fetching artist information..."):            
 | 
				
			||||||
 | 
					                (raw, artistInfo) = ZSpotify.invoke_url(f'{data["href"]}')
 | 
				
			||||||
 | 
					            if ZSpotify.CONFIG.get_allGenres() and len(artistInfo[GENRES]) > 0:
 | 
				
			||||||
 | 
					                for genre in artistInfo[GENRES]:
 | 
				
			||||||
 | 
					                    genres.append(genre)
 | 
				
			||||||
 | 
					            elif len(artistInfo[GENRES]) > 0:
 | 
				
			||||||
 | 
					                genres.append(artistInfo[GENRES][0])
 | 
				
			||||||
 | 
					         
 | 
				
			||||||
 | 
					        if len(genres) == 0:
 | 
				
			||||||
 | 
					            Printer.print(PrintChannel.SKIPS, '###    No Genre found.')
 | 
				
			||||||
 | 
					            genres.append('')   
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
        album_name = info[TRACKS][0][ALBUM][NAME]
 | 
					        album_name = info[TRACKS][0][ALBUM][NAME]
 | 
				
			||||||
        name = info[TRACKS][0][NAME]
 | 
					        name = info[TRACKS][0][NAME]
 | 
				
			||||||
        image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL]
 | 
					        image_url = info[TRACKS][0][ALBUM][IMAGES][0][URL]
 | 
				
			||||||
@ -54,7 +72,7 @@ def get_song_info(song_id) -> Tuple[List[str], str, str, Any, Any, Any, Any, Any
 | 
				
			|||||||
        is_playable = info[TRACKS][0][IS_PLAYABLE]
 | 
					        is_playable = info[TRACKS][0][IS_PLAYABLE]
 | 
				
			||||||
        duration_ms = info[TRACKS][0][DURATION_MS]
 | 
					        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
 | 
					        return artists, genres, album_name, name, image_url, release_year, disc_number, track_number, scraped_song_id, is_playable, duration_ms
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        raise ValueError(f'Failed to parse TRACKS_URL response: {str(e)}\n{raw}')
 | 
					        raise ValueError(f'Failed to parse TRACKS_URL response: {str(e)}\n{raw}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,9 +100,12 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
 | 
				
			|||||||
    try:
 | 
					    try:
 | 
				
			||||||
        output_template = ZSpotify.CONFIG.get_output(mode)
 | 
					        output_template = ZSpotify.CONFIG.get_output(mode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (artists, album_name, name, image_url, release_year, disc_number,
 | 
					        (artists, genres, album_name, name, image_url, release_year, disc_number,
 | 
				
			||||||
         track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id)
 | 
					         track_number, scraped_song_id, is_playable, duration_ms) = get_song_info(track_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        prepareDownloadLoader = Loader("Preparing download...");
 | 
				
			||||||
 | 
					        prepareDownloadLoader.start()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name)
 | 
					        song_name = fix_filename(artists[0]) + ' - ' + fix_filename(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for k in extra_keys:
 | 
					        for k in extra_keys:
 | 
				
			||||||
@ -131,12 +152,15 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
 | 
				
			|||||||
    else:
 | 
					    else:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            if not is_playable:
 | 
					            if not is_playable:
 | 
				
			||||||
 | 
					                prepareDownloadLoader.stop();
 | 
				
			||||||
                Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE)   ###' + "\n")
 | 
					                Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG IS UNAVAILABLE)   ###' + "\n")
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                if check_id and check_name and ZSpotify.CONFIG.get_skip_existing_files():
 | 
					                if check_id and check_name and ZSpotify.CONFIG.get_skip_existing_files():
 | 
				
			||||||
 | 
					                    prepareDownloadLoader.stop();
 | 
				
			||||||
                    Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS)   ###' + "\n")
 | 
					                    Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG ALREADY EXISTS)   ###' + "\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded():
 | 
					                elif check_all_time and ZSpotify.CONFIG.get_skip_previously_downloaded():
 | 
				
			||||||
 | 
					                    prepareDownloadLoader.stop();
 | 
				
			||||||
                    Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG ALREADY DOWNLOADED ONCE)   ###' + "\n")
 | 
					                    Printer.print(PrintChannel.SKIPS, '\n###   SKIPPING: ' + song_name + ' (SONG ALREADY DOWNLOADED ONCE)   ###' + "\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
@ -147,6 +171,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
 | 
				
			|||||||
                    create_download_directory(filedir)
 | 
					                    create_download_directory(filedir)
 | 
				
			||||||
                    total_size = stream.input_stream.size
 | 
					                    total_size = stream.input_stream.size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    prepareDownloadLoader.stop();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    time_start = time.time()
 | 
					                    time_start = time.time()
 | 
				
			||||||
                    downloaded = 0
 | 
					                    downloaded = 0
 | 
				
			||||||
                    with open(filename_temp, 'wb') as file, Printer.progress(
 | 
					                    with open(filename_temp, 'wb') as file, Printer.progress(
 | 
				
			||||||
@ -170,7 +196,7 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
 | 
				
			|||||||
                    time_downloaded = time.time()
 | 
					                    time_downloaded = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    convert_audio_format(filename_temp)
 | 
					                    convert_audio_format(filename_temp)
 | 
				
			||||||
                    set_audio_tags(filename_temp, artists, name, album_name, release_year, disc_number, track_number)
 | 
					                    set_audio_tags(filename_temp, artists, genres, name, album_name, release_year, disc_number, track_number)
 | 
				
			||||||
                    set_music_thumbnail(filename_temp, image_url)
 | 
					                    set_music_thumbnail(filename_temp, image_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if filename_temp != filename:
 | 
					                    if filename_temp != filename:
 | 
				
			||||||
@ -196,8 +222,8 @@ def download_track(mode: str, track_id: str, extra_keys={}, disable_progressbar=
 | 
				
			|||||||
            Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
 | 
					            Printer.print(PrintChannel.ERRORS, "".join(traceback.TracebackException.from_exception(e).format()) + "\n")
 | 
				
			||||||
            if os.path.exists(filename_temp):
 | 
					            if os.path.exists(filename_temp):
 | 
				
			||||||
                os.remove(filename_temp)
 | 
					                os.remove(filename_temp)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					    prepareDownloadLoader.stop()
 | 
				
			||||||
def convert_audio_format(filename) -> None:
 | 
					def convert_audio_format(filename) -> None:
 | 
				
			||||||
    """ Converts raw audio into playable file """
 | 
					    """ Converts raw audio into playable file """
 | 
				
			||||||
    temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
 | 
					    temp_filename = f'{os.path.splitext(filename)[0]}.tmp'
 | 
				
			||||||
@ -224,6 +250,9 @@ def convert_audio_format(filename) -> None:
 | 
				
			|||||||
        inputs={temp_filename: None},
 | 
					        inputs={temp_filename: None},
 | 
				
			||||||
        outputs={filename: output_params}
 | 
					        outputs={filename: output_params}
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    ff_m.run()
 | 
					    
 | 
				
			||||||
 | 
					    with Loader("Converting file..."):
 | 
				
			||||||
 | 
					        ff_m.run()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
    if os.path.exists(temp_filename):
 | 
					    if os.path.exists(temp_filename):
 | 
				
			||||||
        os.remove(temp_filename)
 | 
					        os.remove(temp_filename)
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ from typing import List, Tuple
 | 
				
			|||||||
import music_tag
 | 
					import music_tag
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from const import ARTIST, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
 | 
					from const import ARTIST, GENRE, TRACKTITLE, ALBUM, YEAR, DISCNUMBER, TRACKNUMBER, ARTWORK, \
 | 
				
			||||||
    WINDOWS_SYSTEM, ALBUMARTIST
 | 
					    WINDOWS_SYSTEM, ALBUMARTIST
 | 
				
			||||||
from zspotify import ZSpotify
 | 
					from zspotify import ZSpotify
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -124,11 +124,12 @@ def clear() -> None:
 | 
				
			|||||||
        os.system('clear')
 | 
					        os.system('clear')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def set_audio_tags(filename, artists, name, album_name, release_year, disc_number, track_number) -> None:
 | 
					def set_audio_tags(filename, artists, genres, name, album_name, release_year, disc_number, track_number) -> None:
 | 
				
			||||||
    """ sets music_tag metadata """
 | 
					    """ sets music_tag metadata """
 | 
				
			||||||
    tags = music_tag.load_file(filename)
 | 
					    tags = music_tag.load_file(filename)
 | 
				
			||||||
    tags[ALBUMARTIST] = artists[0]
 | 
					    tags[ALBUMARTIST] = artists[0]
 | 
				
			||||||
    tags[ARTIST] = conv_artist_format(artists)
 | 
					    tags[ARTIST] = conv_artist_format(artists)
 | 
				
			||||||
 | 
					    tags[GENRE] = genres[0] if not ZSpotify.CONFIG.get_allGenres() else ZSpotify.CONFIG.get_allGenresDelimiter().join(genres)
 | 
				
			||||||
    tags[TRACKTITLE] = name
 | 
					    tags[TRACKTITLE] = name
 | 
				
			||||||
    tags[ALBUM] = album_name
 | 
					    tags[ALBUM] = album_name
 | 
				
			||||||
    tags[YEAR] = release_year
 | 
					    tags[YEAR] = release_year
 | 
				
			||||||
@ -279,3 +280,84 @@ def fmt_seconds(secs: float) -> str:
 | 
				
			|||||||
        return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
 | 
					        return f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
 | 
					        return f'{h}'.zfill(2) + ':' + f'{m}'.zfill(2) + ':' + f'{s}'.zfill(2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# load symbol from:
 | 
				
			||||||
 | 
					# https://stackoverflow.com/questions/22029562/python-how-to-make-simple-animated-loading-while-process-is-running
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# imports
 | 
				
			||||||
 | 
					from itertools import cycle
 | 
				
			||||||
 | 
					from shutil import get_terminal_size
 | 
				
			||||||
 | 
					from threading import Thread
 | 
				
			||||||
 | 
					from time import sleep
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Loader:
 | 
				
			||||||
 | 
					    """Busy symbol.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Can be called inside a context:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with Loader("This take some Time..."):
 | 
				
			||||||
 | 
					        # do something
 | 
				
			||||||
 | 
					        pass          
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def __init__(self, desc="Loading...", end='', timeout=0.1, mode='std1'):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        A loader-like context manager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Args:
 | 
				
			||||||
 | 
					            desc (str, optional): The loader's description. Defaults to "Loading...".
 | 
				
			||||||
 | 
					            end (str, optional): Final print. Defaults to "".
 | 
				
			||||||
 | 
					            timeout (float, optional): Sleep time between prints. Defaults to 0.1.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.desc = desc
 | 
				
			||||||
 | 
					        self.end = end
 | 
				
			||||||
 | 
					        self.timeout = timeout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._thread = Thread(target=self._animate, daemon=True)
 | 
				
			||||||
 | 
					        if mode == 'std1':
 | 
				
			||||||
 | 
					            self.steps = ["⢿", "⣻", "⣽", "⣾", "⣷", "⣯", "⣟", "⡿"]
 | 
				
			||||||
 | 
					        elif mode == 'std2':
 | 
				
			||||||
 | 
					            self.steps = ["◜","◝","◞","◟"]
 | 
				
			||||||
 | 
					        elif mode == 'std3':
 | 
				
			||||||
 | 
					            self.steps = ["😐 ","😐 ","😮 ","😮 ","😦 ","😦 ","😧 ","😧 ","🤯 ","💥 ","✨ ","\u3000 ","\u3000 ","\u3000 "]
 | 
				
			||||||
 | 
					        elif mode == 'prog':
 | 
				
			||||||
 | 
					            self.steps = ["[∙∙∙]","[●∙∙]","[∙●∙]","[∙∙●]","[∙∙∙]"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.done = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def start(self):
 | 
				
			||||||
 | 
					        self._thread.start()
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _animate(self):
 | 
				
			||||||
 | 
					        for c in cycle(self.steps):
 | 
				
			||||||
 | 
					            if self.done:
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					            print(f"\r\t{c} {self.desc} ", flush=True, end="")
 | 
				
			||||||
 | 
					            sleep(self.timeout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __enter__(self):
 | 
				
			||||||
 | 
					        self.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def stop(self):
 | 
				
			||||||
 | 
					        self.done = True
 | 
				
			||||||
 | 
					        cols = get_terminal_size((80, 20)).columns
 | 
				
			||||||
 | 
					        print("\r" + " " * cols, end="", flush=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.end != "":
 | 
				
			||||||
 | 
					            print(f"\r{self.end}", flush=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __exit__(self, exc_type, exc_value, tb):
 | 
				
			||||||
 | 
					        # handle exceptions with those variables ^
 | 
				
			||||||
 | 
					        self.stop()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    with Loader("Loading with context manager..."):
 | 
				
			||||||
 | 
					        for i in range(10):
 | 
				
			||||||
 | 
					            sleep(0.25)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loader = Loader("Loading with object...", "That was fast!", 0.05).start()
 | 
				
			||||||
 | 
					    for i in range(10):
 | 
				
			||||||
 | 
					        sleep(0.25)
 | 
				
			||||||
 | 
					    loader.stop()
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ It's like youtube-dl, but for Spotify.
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import os.path
 | 
					import os.path
 | 
				
			||||||
from getpass import getpass
 | 
					from getpass import getpass
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
import requests
 | 
					import requests
 | 
				
			||||||
from librespot.audio.decoders import VorbisOnlyAudioQuality
 | 
					from librespot.audio.decoders import VorbisOnlyAudioQuality
 | 
				
			||||||
from librespot.core import Session
 | 
					from librespot.core import Session
 | 
				
			||||||
@ -19,8 +19,7 @@ from const import TYPE, \
 | 
				
			|||||||
    PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ
 | 
					    PLAYLIST_READ_PRIVATE, USER_LIBRARY_READ
 | 
				
			||||||
from config import Config
 | 
					from config import Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ZSpotify:    
 | 
				
			||||||
class ZSpotify:
 | 
					 | 
				
			||||||
    SESSION: Session = None
 | 
					    SESSION: Session = None
 | 
				
			||||||
    DOWNLOAD_QUALITY = None
 | 
					    DOWNLOAD_QUALITY = None
 | 
				
			||||||
    CONFIG: Config = Config()
 | 
					    CONFIG: Config = Config()
 | 
				
			||||||
@ -82,10 +81,21 @@ class ZSpotify:
 | 
				
			|||||||
        return requests.get(url, headers=headers, params=params).json()
 | 
					        return requests.get(url, headers=headers, params=params).json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def invoke_url(cls, url):
 | 
					    def invoke_url(cls, url, tryCount = 0):
 | 
				
			||||||
 | 
					        # we need to import that here, otherwise we will get circular imports!
 | 
				
			||||||
 | 
					        from termoutput import Printer, PrintChannel                
 | 
				
			||||||
        headers = cls.get_auth_header()
 | 
					        headers = cls.get_auth_header()
 | 
				
			||||||
        response = requests.get(url, headers=headers)
 | 
					        response = requests.get(url, headers=headers)
 | 
				
			||||||
        return response.text, response.json()
 | 
					        responseText = response.text
 | 
				
			||||||
 | 
					        responseJson = response.json()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if 'error' in responseJson and tryCount < 5:
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Printer.Print(PrintChannel.API_ERROR, f"Spotify API Error ({responseJson['error']['status']}): {responseJson['error']['message']}")            
 | 
				
			||||||
 | 
					            time.sleep(5)
 | 
				
			||||||
 | 
					            return cls.invoke_url(url, tryCount + 1)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					        return responseText, responseJson
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def check_premium(cls) -> bool:
 | 
					    def check_premium(cls) -> bool:
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user