"""Download free albums and tracks from Bandcamp Usage: bcdl-free setdefault [-d ] [-e ] [-z ] [-c ] [-f ] bcdl-free defaults bcdl-free clear bcdl-free [--debug] [--force] [--no-unzip] [-al] [-d ] [-e ] [-z ] [-c ] [-f ] [--cookies ] [--identity ] URL... bcdl-free -h | --help | --version bcdl-free [--debug] [--force] [--no-unzip] [-al] [-d ] [-e ] [-z ] [-c ] [-f ] [--cookies ] [--identity ] [--download-history-file ] URL... Arguments: URL URL to download. Can be a link to a label or release page Subcommands: setdefaults set default configuration options defaults list default configuration options clear clear default configuration options Options: -h --help Show this screen --version Show version --force Download even if album has been downloaded before --no-unzip Don't unzip downloaded albums --debug Set loglevel to debug -a -l Dummy options, for backwards compatibility -d --dir Set download directory -c --country Set country -z --zipcode Set zipcode -e --email Set email (set to 'auto' to automatically download from a disposable email) -f --format Set format --cookies Path to cookies.txt file so albums in your collection can be downloaded --identity Value of identity cookie so albums in your collection can be downloaded --download-history-file Path to history file containing downloaded albums Formats: - FLAC - V0MP3 - 320MP3 - AAC - Ogg - ALAC - WAV - AIFF """ import dataclasses import logging import sys import os import pprint from typing import List, Set from docopt import docopt from configparser import ConfigParser from free_bandcamp_downloader import __version__ from free_bandcamp_downloader.bc_free_downloader import ( AlbumInfo, BCFreeDownloader, BCFreeDownloaderOptions, TralbumId, ) from free_bandcamp_downloader import logger class Config: def __init__(self): config_dir = get_config_dir() # initialize with default config first # this is combined from the dataclass and CLI params self.parser = ConfigParser(allow_no_value=True) self.parser["free-bandcamp-downloader"] = {} for field in dataclasses.fields(BCFreeDownloaderOptions): self.parser["free-bandcamp-downloader"][field.name] = field.default self.parser["free-bandcamp-downloader"]["force"] = "false" self.parser["free-bandcamp-downloader"]["no-unzip"] = "false" self.parser["free-bandcamp-downloader"]["download-history-file"] = ( get_data_dir() + "/downloaded.txt" ) # read config file self.config_path = os.path.join(config_dir, "free-bandcamp-downloader.cfg") if not os.path.exists(self.config_path): with open(self.config_path, "w") as f: self.parser.write(f) return self.parser.read(self.config_path) def get(self, key): return self.parser["free-bandcamp-downloader"].get(key) def set(self, key, value): if value is not None: value = str(value) self.parser["free-bandcamp-downloader"][key] = value def save(self): with open(self.config_path, "w") as f: self.parser.write(f) def __str__(self): return pprint.pformat(dict(self.parser["free-bandcamp-downloader"]), indent=2) def options_from_config(config: Config): options = BCFreeDownloaderOptions() for field in dataclasses.fields(options): setattr( options, field.name, config.parser["free-bandcamp-downloader"][field.name] ) return options def get_config_dir() -> str: if "XDG_CONFIG_HOME" in os.environ: config_dir = os.path.join( os.environ["XDG_CONFIG_HOME"], "free-bandcamp-downloader" ) else: config_dir = os.path.join( os.path.expanduser("~"), ".config", "free-bandcamp-downloader" ) if not os.path.exists(config_dir): os.makedirs(config_dir) return config_dir def get_data_dir() -> str: if "XDG_DATA_HOME" in os.environ: data_dir = os.path.join(os.environ["XDG_DATA_HOME"], "free-bandcamp-downloader") else: data_dir = os.path.join( os.path.expanduser("~"), ".local", "share", "free-bandcamp-downloader" ) if not os.path.exists(data_dir): os.makedirs(data_dir) return data_dir def is_downloaded(downloaded_set, id: TralbumId, url: str = None) -> bool: return id in downloaded_set or url in downloaded_set def add_to_dl_file(config: Config, id: TralbumId): history_file = config.parser["free-bandcamp-downloader"]["download-history-file"] with open(history_file, "a") as f: f.write(f"{id[0][0]}:{id[1]}\n") def get_downloaded(config: Config) -> Set[TralbumId]: history_file = config.parser["free-bandcamp-downloader"]["download-history-file"] if not os.path.exists(history_file): with open(history_file, "w") as f: pass downloaded = set() with open(history_file, "r") as f: for line in f: type = line.strip()[:2] if type == "a:": type = "album" data = int(line[2:]) elif type == "t:": type = "track" data = int(line[2:]) else: type = "url" data = line.strip() downloaded.add((type, data)) return downloaded def post_download(album_info: AlbumInfo, config: Config): file_name = album_info["file_name"] # file list for setting tags files = [file_name] unzip = not config.parser.getboolean("free-bandcamp-downloader", "no-unzip") # unzip if needed if unzip and file_name.endswith(".zip"): files = BCFreeDownloader.unzip_album(file_name) logger.info("Setting tags...") for file in files: BCFreeDownloader.tag_file(file, album_info["head_data"]) def download_urls(urls: List[str], config: Config): downloader = BCFreeDownloader(options_from_config(config)) downloaded = get_downloaded(config) force = config.parser.getboolean("free-bandcamp-downloader", "force") for url in urls: soup = downloader.get_url_soup(url) url_info = downloader.get_page_info(soup) urltype = url_info and url_info.get("type") if urltype == "album" or urltype == "song": tralbum = url_info["info"]["tralbum_data"] type = tralbum["current"]["type"] id = tralbum["current"]["id"] url = tralbum["url"] if not force and is_downloaded(downloaded, (type, id), url): logger.error( f"{url} already downloaded. To download anyways, use --force." ) continue ret = downloader.download_album(soup) if ret["is_downloaded"]: add_to_dl_file(config, (type, id)) downloaded.add((type, id)) post_download(ret, config) elif urltype == "band": for rel in url_info["info"]["releases"]: type = rel["type"] id = rel["id"] url = rel["url"] if not force and is_downloaded(downloaded, (type, id), url): logger.error( f"{url} already downloaded. To download anyways, use --force." ) continue soup = downloader.get_url_soup(url) ret = downloader.download_album(soup) if ret["is_downloaded"]: add_to_dl_file(config, (type, id)) downloaded.add((type, id)) post_download(ret, config) else: continue # finish up downloading ret = downloader.flush_email_downloads() for album_info in ret: type = album_info["tralbum_data"]["current"]["type"] id = album_info["tralbum_data"]["current"]["id"] add_to_dl_file(config, (type, id)) downloaded.add((type, id)) post_download(album_info, config) def main(): config = Config() arguments = docopt(__doc__, version=__version__) if arguments["--debug"]: logger.setLevel(logging.DEBUG) # set config if needed if arguments["URL"] or arguments["setdefault"]: for option in config.parser["free-bandcamp-downloader"].keys(): arg = f"--{option}" if arguments.get(arg): config.set(option, arguments[arg]) if ( config.parser["free-bandcamp-downloader"]["format"] not in BCFreeDownloader.FORMATS ): logger.error( f'{config.parser.get("format")} is not a valid format. See "bcdl-free -h" for valid formats' ) sys.exit(1) # write to config file if arguments["setdefault"]: config.save() sys.exit(0) if arguments["clear"]: with open(config.config_path, "w"): pass sys.exit(0) if arguments["defaults"]: print(str(config)) sys.exit(0) if arguments["URL"]: download_urls(arguments["URL"], config) if __name__ == "__main__": main()