""" Module that handles the command line arguments. """ import argparse import sys import textwrap from argparse import ArgumentParser, Namespace, _ArgumentGroup from typing import List from spotdl import _version from spotdl.download.downloader import AUDIO_PROVIDERS, LYRICS_PROVIDERS from spotdl.utils.ffmpeg import FFMPEG_FORMATS from spotdl.utils.formatter import VARS from spotdl.utils.logging import NAME_TO_LEVEL __all__ = ["OPERATIONS", "SmartFormatter", "parse_arguments"] OPERATIONS = ["download", "save", "web", "sync", "meta", "url"] class SmartFormatter(argparse.HelpFormatter): """ Class that overrides the default help formatter. """ def _split_lines(self, text: str, width: int) -> List[str]: """ Split the text in multiple lines if a line starts with a N| """ if text.startswith("N|"): return text[2:].splitlines() text = self._whitespace_matcher.sub(" ", text).strip() return textwrap.wrap(text, width) def parse_main_options(parser: _ArgumentGroup): """ Parse main options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add operation argument operation = parser.add_argument( "operation", choices=OPERATIONS, default="download", const="download", nargs="?", help=( "N|The operation to perform.\n" "download: Download the songs to the disk and embed metadata.\n" "save: Saves the songs metadata to a file for further use.\n" "web: Starts a web interface to simplify the download process.\n" "sync: Removes songs that are no longer present, downloads new ones\n" "meta: Update your audio files with metadata\n" "url: Get the download URL for songs\n\n" ), ) # Add query argument query = parser.add_argument( "query", nargs="+", type=str, help=( "N|Spotify/YouTube URL for a song/playlist/album/artist/etc. to download.\n\n" "For album/playlist/artist searching, include 'album:', 'playlist:', 'artist:' \n" "(ie. 'album:the album name' you can mix these options to get more accurate results)" ".\n\n" "To download liked songs use 'saved' as the query, to download all user playlists\n" "use 'all-user-playlists, to download playlists that the user has created\n" "use 'all-saved-playlists', to download all user liked playlists\n" "use 'all-user-followed-artists', to download all user saved albums " "use 'all-user-saved-albums' \n\n" "For manual audio matching, you can use the format 'YouTubeURL|SpotifyURL'\n" "You can only use album/playlist/tracks urls when " "downloading/matching youtube urls.\n" "When using youtube url without spotify url, " "you won't be able to use `--fetch-albums` option.\n\n" ), ) try: is_web = sys.argv[1] == "web" except IndexError: is_web = False is_frozen = getattr(sys, "frozen", False) # If the program is frozen, we and user didn't pass any arguments, # or if the user is using the web interface, we don't need to parse # the query if (is_frozen and len(sys.argv) < 2) or (len(sys.argv) > 1 and is_web): # If we are running the web interface # or we are in the frozen env and not running web interface # don't remove the operation from the arg parser if not is_web or (is_frozen and not is_web): parser._remove_action(operation) # pylint: disable=protected-access parser._remove_action(query) # pylint: disable=protected-access # Audio provider argument parser.add_argument( "--audio", dest="audio_providers", nargs="*", choices=AUDIO_PROVIDERS, help="The audio provider to use. You can provide more than one for fallback.", ) # Lyrics provider argument parser.add_argument( "--lyrics", dest="lyrics_providers", nargs="*", choices=LYRICS_PROVIDERS.keys(), help=( "The lyrics provider to use. You can provide more than one for fallback. " "Synced lyrics might not work correctly with some music players. " "For such cases it's better to use `--generate-lrc` option." ), ) parser.add_argument( "--genius-access-token", dest="genius_token", help="Lets you choose your own Genius access token.", ) # Add config argument parser.add_argument( "--config", action="store_true", help=( "Use the config file to download songs. " "It's located under C:\\Users\\user\\.spotdl\\config.json " "or ~/.spotdl/config.json under linux" ), ) # Add search query argument parser.add_argument( "--search-query", help=f"The search query to use, available variables: {', '.join(VARS)}", type=str, ) # Add don't filter results argument parser.add_argument( "--dont-filter-results", dest="filter_results", action="store_const", const=False, help="Disable filtering results.", ) # Add use only verified results argument parser.add_argument( "--album-type", choices={"album", "single"}, help="Type of the album to search for. (album, single)", type=str, ) # Add use only verified results argument parser.add_argument( "--only-verified-results", action="store_const", const=True, help="Use only verified results. (Not all providers support this)", ) def parse_spotify_options(parser: _ArgumentGroup): """ Parse spotify options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add login argument parser.add_argument( "--user-auth", action="store_const", const=True, help="Login to Spotify using OAuth.", ) # Add client id argument parser.add_argument( "--client-id", help="The client id to use when logging in to Spotify.", type=str, ) # Add client secret argument parser.add_argument( "--client-secret", help="The client secret to use when logging in to Spotify.", type=str, ) # Add auth token argument parser.add_argument( "--auth-token", help="The authorization token to use directly to log in to Spotify.", type=str, ) # Add cache path argument parser.add_argument( "--cache-path", type=str, help="The path where spotipy cache file will be stored.", ) # Add no cache argument parser.add_argument( "--no-cache", action="store_const", const=True, help="Disable caching (both requests and token).", ) # Add max retries argument parser.add_argument( "--max-retries", type=int, help="The maximum number of retries to perform when getting metadata.", ) # Add headless argument parser.add_argument( "--headless", action="store_const", const=True, help="Run in headless mode.", ) # Add use cache file argument parser.add_argument( "--use-cache-file", action="store_const", const=True, help=( "Use the cache file to get metadata. " "It's located under C:\\Users\\user\\.spotdl\\.spotify_cache " "or ~/.spotdl/.spotify_cache under linux. " "It only caches tracks and " "gets updated whenever spotDL gets metadata from Spotify. " "(It may provide outdated metadata use with caution)" ), ) def parse_ffmpeg_options(parser: _ArgumentGroup): """ Parse ffmpeg options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add ffmpeg executable argument parser.add_argument( "--ffmpeg", help="The ffmpeg executable to use.", type=str, ) # Add search threads argument parser.add_argument( "--threads", type=int, help="The number of threads to use when downloading songs.", ) # Add constant bit rate argument parser.add_argument( "--bitrate", choices=[ "auto", "disable", "8k", "16k", "24k", "32k", "40k", "48k", "64k", "80k", "96k", "112k", "128k", "160k", "192k", "224k", "256k", "320k", ] + list(map(str, range(0, 10))), type=str.lower, help=( "The constant/variable bitrate to use for the output file. " "Values from 0 to 9 are variable bitrates. " "Auto will use the bitrate of the original file. " "Disable will disable the bitrate option. " "(In case of m4a and opus files, auto and disable will skip the conversion)" ), ) # Additional ffmpeg arguments parser.add_argument( "--ffmpeg-args", type=str, help="Additional ffmpeg arguments passed as a string.", ) def parse_output_options(parser: _ArgumentGroup): """ Parse output options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add output format argument parser.add_argument( "--format", choices=FFMPEG_FORMATS.keys(), help="The format to download the song in.", type=str, ) # Add save file argument parser.add_argument( "--save-file", type=str, help=( "The file to save/load the songs data from/to. " "It has to end with .spotdl. " "If combined with the download operation, it will save the songs data to the file. " "Required for save/sync (use - to print to stdout when using save). " ), required=len(sys.argv) > 1 and sys.argv[1] in ["save"], ) # Add preload argument parser.add_argument( "--preload", action="store_const", const=True, help="Preload the download url to speed up the download process.", ) # Add name format argument parser.add_argument( "--output", type=str, help=f"Specify the downloaded file name format, available variables: {', '.join(VARS)}", ) # Add m3u argument parser.add_argument( "--m3u", type=str, nargs="?", help=( "Name of the m3u file to save the songs to. " "Defaults to {list[0]}.m3u8 " "If you want to generate a m3u for each list in the query use {list}, " "If you want to generate a m3u file based on the first list in the query use {list[0]}" ", (0 is the first list in the query, 1 is the second, etc. " "songs don't count towards the list number) " ), const="{list[0]}.m3u8", ) # Add cookie file argument parser.add_argument( "--cookie-file", help="Path to cookies file.", type=str, ) # Add overwrite argument parser.add_argument( "--overwrite", choices={"force", "skip", "metadata"}, help=( "How to handle existing/duplicate files. " "(When combined with --scan-for-songs force will remove " "all duplicates, and metadata will only apply metadata to the " "latest song and will remove the rest. )" ), type=str, ) # Option to increase compatibility of filenames and easier handling in the shell parser.add_argument( "--restrict", choices={"strict", "ascii", "none"}, const="strict", nargs="?", help="Restrict filenames to a sanitized set of characters for better compatibility", type=str, ) # Option to print errors on exit, useful for long playlist parser.add_argument( "--print-errors", action="store_const", const=True, help="Print errors (wrong songs, failed downloads etc) on exit, useful for long playlist", ) # Option to save errors to a file parser.add_argument( "--save-errors", type=str, help="Save errors (wrong songs, failed downloads etc) to a file", ) # Option to use sponsor block parser.add_argument( "--sponsor-block", action="store_const", const=True, help="Use the sponsor block to download songs from yt/ytm.", ) # Add archive_file argument parser.add_argument( "--archive", type=str, help="Specify the file name for an archive of already downloaded songs", ) # Option to set the track number & album of tracks in a playlist to their index in the playlist # & the name of playlist respectively. parser.add_argument( "--playlist-numbering", action="store_const", dest="playlist_numbering", const=True, help="Sets each track in a playlist to have the playlist's name as its album,\ and album art as the playlist's icon", ) # Option to set the track number & album of tracks, while retaining album art of each track, in # a playlist to their index in the playlist & the name of playlist respectively. parser.add_argument( "--playlist-retain-track-cover", action="store_const", dest="playlist_retain_track_cover", const=True, help="Sets each track in a playlist to have the playlist's name as its album,\ while retaining album art of each track", ) # Option to scan the output directory for existing files parser.add_argument( "--scan-for-songs", action="store_const", const=True, help=( "Scan the output directory for existing files. " "This option should be combined with the --overwrite option " "to control how existing files are handled. (Output directory is the last directory " "that is not a template variable in the output template)" ), ) # Option to fetch all albums from songs in query parser.add_argument( "--fetch-albums", action="store_const", const=True, help="Fetch all albums from songs in query", ) # Option to change the id3 separator parser.add_argument( "--id3-separator", type=str, help="Change the separator used in the id3 tags. Only supported for mp3 files.", ) # Option to use ytm data instead of spotify data # when downloading using ytm link parser.add_argument( "--ytm-data", action="store_const", const=True, help="Use ytm data instead of spotify data when downloading using ytm link.", ) # Option whether to add unavailable songs to the m3u file parser.add_argument( "--add-unavailable", action="store_const", const=True, help="Add unavailable songs to the m3u/archive files when downloading", ) # Generate lrc files parser.add_argument( "--generate-lrc", action="store_const", const=True, help=( "Generate lrc files for downloaded songs. " "Requires `synced` provider to be present in the lyrics providers list." ), ) # Force update metadata parser.add_argument( "--force-update-metadata", action="store_const", const=True, help="Force update metadata for songs that already have metadata.", ) # Sync without deleting parser.add_argument( "--sync-without-deleting", action="store_const", const=True, help="Sync without deleting songs that are not in the query.", ) # Max file name length parser.add_argument( "--max-filename-length", type=int, help=( "Max file name length. " "(This won't override the max file name length enforced by the OS)" ), ) # YT-DlP options parser.add_argument( "--yt-dlp-args", type=str, help="Arguments to pass to yt-dlp", ) # Detect formats option parser.add_argument( "--detect-formats", type=str, nargs="*", help=( "Detect already downloaded songs with file format different from the --format option " "(When combined with --m3u option, " "only first detected format will be added to m3u file)" ), choices=FFMPEG_FORMATS.keys(), ) # download song in meta operation parser.add_argument( "--redownload", action="store_const", const=True, help="to redownload the local song in diffrent format using --format for meta operation", ) # skip album art for meta operation parser.add_argument( "--skip-album-art", action="store_const", const=True, help="skip downloading album art for meta operation", ) # Ignore songs from a paticular album parser.add_argument( "--ignore-albums", type=str, nargs="*", help="ignores the song of the given albums", ) # Skip explicit songs options parser.add_argument( "--skip-explicit", action="store_const", const=True, help="Skip explicit songs" ) parser.add_argument( "--proxy", help="Http(s) proxy server for download song. Example: http://host:port", ) # Skip songs having a skip flag file parser.add_argument( "--create-skip-file", action="store_const", const=True, help="Create skip file for successfully downloaded file", ) # Skip songs having a skip flag file parser.add_argument( "--respect-skip-file", action="store_const", const=True, help="If a file with the extension .skip exists, skip download", ) # Sync remove lrc files parser.add_argument( "--sync-remove-lrc", action="store_const", const=True, help="Remove lrc files when using sync operation when downloading songs", ) def parse_web_options(parser: _ArgumentGroup): """ Parse web options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add host argument parser.add_argument( "--host", type=str, help="The host to use for the web server.", ) # Add port argument parser.add_argument( "--port", type=int, help="The port to run the web server on.", ) # Add keep alive argument parser.add_argument( "--keep-alive", action="store_const", const=True, help="Keep the web server alive even when no clients are connected.", ) # Add allowed origins argument parser.add_argument( "--allowed-origins", nargs="*", help="The allowed origins for the web server.", ) # Add use output directory argument parser.add_argument( "--web-use-output-dir", action="store_const", const=True, help=( "Use the output directory instead of the session directory for downloads. (" "This might cause issues if you have multiple users using the web-ui at the same time)" ), ) # Add keep sessions argument parser.add_argument( "--keep-sessions", action="store_const", const=True, help="Keep the session directory after the web server is closed.", ) # Add keep sessions argument parser.add_argument( "--force-update-gui", action="store_const", const=True, default=False, help="Refresh the web server directory with a fresh git checkout", ) # Add custom web gui repo parser.add_argument( "--web-gui-repo", type=str, help=( "Custom web gui repo to use for the web server. " "Example: https://github.com/spotdl/web-ui/tree/master/dist" ), ) # Add custom web gui repo parser.add_argument( "--web-gui-location", type=str, help="Path to the web gui directory to use for the web server.", ) # Enable TLS for the web server parser.add_argument( "--enable-tls", action="store_const", const=True, help="Enable TLS on the web server.", ) # Add File Location of the TLS Certificate file (Pem Format) parser.add_argument( "--cert-file", type=str, help="File Path to the TLS Certificate (PEM format)." ) # Add File Location of the TLS Private Key file (Pem Format) parser.add_argument( "--key-file", type=str, help="File Path to the TLS Private Key (PEM format)." ) # Add File Location of the TLS Certificate Authority file (Pem Format) parser.add_argument( "--ca-file", type=str, help="File Path to the TLS Certificate Authority File (PEM format).", ) def parse_misc_options(parser: _ArgumentGroup): """ Parse misc options from the command line. ### Arguments - parser: The argument parser to add the options to. """ # Add verbose argument parser.add_argument( "--log-level", choices=NAME_TO_LEVEL.keys(), help="Select log level.", ) # Add simple tui argument parser.add_argument( "--simple-tui", action="store_const", const=True, help="Use a simple tui.", ) # Add log format argument parser.add_argument( "--log-format", help=( "Custom logging format to use. More info: " "https://docs.python.org/3/library/logging.html#logrecord-attributes" ), ) def parse_other_options(parser: _ArgumentGroup): """ Parse other options from the command line. ### Arguments - parser: The argument parser to add the options to. """ parser.add_argument( "--download-ffmpeg", action="store_true", help="Download ffmpeg to spotdl directory.", ) parser.add_argument( "--generate-config", action="store_true", help="Generate a config file. This will overwrite current config if present.", ) parser.add_argument( "--check-for-updates", action="store_true", help="Check for new version." ) parser.add_argument( "--profile", action="store_true", help="Run in profile mode. Useful for debugging.", ) parser.add_argument( "--version", "-v", action="version", help="Show the version number and exit.", version=_version.__version__, ) def create_parser() -> ArgumentParser: """ Parse arguments from the command line. ### Returns - A Namespace object containing the parsed arguments. """ # Initialize argument parser parser = ArgumentParser( prog="spotdl", description="Download your Spotify playlists and songs along with album art and metadata", formatter_class=SmartFormatter, epilog=( "For more information, visit http://spotdl.rtfd.io/ " "or join our Discord server: https://discord.com/invite/xCa23pwJWY" ), ) # Parse main options main_options = parser.add_argument_group("Main options") parse_main_options(main_options) # Parse spotify options spotify_options = parser.add_argument_group("Spotify options") parse_spotify_options(spotify_options) # Parse ffmpeg options ffmpeg_options = parser.add_argument_group("FFmpeg options") parse_ffmpeg_options(ffmpeg_options) # Parse output options output_options = parser.add_argument_group("Output options") parse_output_options(output_options) # Parse web options web_options = parser.add_argument_group("Web options") parse_web_options(web_options) # Parse misc options misc_options = parser.add_argument_group("Misc options") parse_misc_options(misc_options) # Parse other options other_options = parser.add_argument_group("Other options") parse_other_options(other_options) return parser def parse_arguments() -> Namespace: """ Parse arguments from the command line. ### Arguments - parser: The argument parser to parse the arguments from. ### Returns - A Namespace object containing the parsed arguments. """ # Create parser parser = create_parser() # Parse arguments return parser.parse_args()