From 29d0597d93733252f1d7701e84376fa1c47d6519 Mon Sep 17 00:00:00 2001 From: 7x11x13 Date: Thu, 30 Nov 2023 17:59:28 -0500 Subject: [PATCH 1/2] Rewrite --- .gitignore | 3 +- README.md | 4 - free_bandcamp_downloader/__init__.py | 71 +---- free_bandcamp_downloader/__main__.py | 421 +++++++++++++++++++++++++++ free_bandcamp_downloader/main.py | 390 ------------------------- poetry.lock | 389 +++++++++++++++++++++++++ pyproject.toml | 25 ++ setup.py | 22 -- 8 files changed, 840 insertions(+), 485 deletions(-) create mode 100644 free_bandcamp_downloader/__main__.py delete mode 100644 free_bandcamp_downloader/main.py create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 58c45a8..f935c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ env *.egg-info *.log -__pycache__ \ No newline at end of file +__pycache__ +notes \ No newline at end of file diff --git a/README.md b/README.md index f5ed05f..ac536e3 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,6 @@ and tags them with data from the Bandcamp page. ## Installation -Requires Firefox and [geckodriver](https://github.com/mozilla/geckodriver/releases) - -Make sure geckodriver is in your PATH environment variable - Install with pip ``` pip install git+https://github.com/7x11x13/free-bandcamp-downloader diff --git a/free_bandcamp_downloader/__init__.py b/free_bandcamp_downloader/__init__.py index 9025258..3e1afdc 100644 --- a/free_bandcamp_downloader/__init__.py +++ b/free_bandcamp_downloader/__init__.py @@ -1,73 +1,8 @@ -import atexit +import importlib.metadata import logging import os -from configparser import ConfigParser -logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO')) +logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) logger = logging.getLogger(__name__) -__version__ = 'v0.0.7' - -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 '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') - -download_history_file = os.path.join(data_dir, 'downloaded.txt') - -default_config = \ - f"""[free-bandcamp-downloader] - country = United States - zipcode = 00000 - email = auto - format = FLAC - dir = . - download_history_file = {download_history_file}""" - -config_file = os.path.join(config_dir, 'free-bandcamp-downloader.cfg') - -if not os.path.exists(config_file): - if not os.path.exists(config_dir): - os.makedirs(config_dir) - with open(config_file, 'w') as f: - f.write(default_config) - -if not os.path.exists(download_history_file): - if not os.path.exists(data_dir): - os.makedirs(data_dir) - with open(download_history_file, 'w') as f: - pass - -parser = ConfigParser() -parser.read(config_file) - - -class Config: - def __init__(self, parser: ConfigParser): - self.parser = parser - atexit.register(self.save) - - def get(self, key): - return self.parser['free-bandcamp-downloader'].get(key, None) - - def set(self, key, value): - self.parser['free-bandcamp-downloader'][key] = value - - def save(self): - with open(config_file, 'w') as f: - self.parser.write(f) - - def __str__(self): - return str(dict(self.parser['free-bandcamp-downloader'])) - - -config = Config(parser) +__version__ = importlib.metadata.version("free-bandcamp-downloader") diff --git a/free_bandcamp_downloader/__main__.py b/free_bandcamp_downloader/__main__.py new file mode 100644 index 0000000..d27c21a --- /dev/null +++ b/free_bandcamp_downloader/__main__.py @@ -0,0 +1,421 @@ +"""Download free albums and tracks from Bandcamp +Usage: + bcdl-free (-a | -l )[--force][--no-unzip][-d | --dir ][-e | --email ] + [-z | --zipcode ][-c | --country ][-f | --format ] + bcdl-free setdefault [-d | --dir ][-e | --email ][-z | --zipcode ] + [-c | --country ][-f | --format ] + bcdl-free defaults + bcdl-free clear + bcdl-free (-h | --help) + bcdl-free --version +Options: + -h --help Show this screen + --version Show version + -a Download the album at URL + -l Download all free albums of the label at URL + --force Download even if album has been downloaded before + --no-unzip Don't unzip downloaded albums + setdefault Set default options + defaults List the default options + clear Clear download history + -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 +Formats: + - FLAC + - V0MP3 + - 320MP3 + - AAC + - Ogg + - ALAC + - WAV + - AIFF +""" + +import atexit +import dataclasses +import glob +import json +import os +import pprint +import re +import sys +import time +import zipfile +from configparser import ConfigParser +from dataclasses import dataclass +from typing import Dict, Set +from urllib.parse import urljoin, urlsplit + +import mutagen +import pyrfc6266 +import requests +import secmail +from bs4 import BeautifulSoup +from docopt import docopt +from tqdm import tqdm + +from free_bandcamp_downloader import __version__, logger + + +@dataclass +class BCFreeDownloaderOptions: + country: str = None + zipcode: str = None + email: str = None + format: str = None + dir: str = None + + +@dataclass +class BCFreeDownloaderAlbumData: + about: str = None + credits: str = None + tags: str = None + + +class BCFreeDownloadError(Exception): + pass + + +class BCFreeDownloader: + CHUNK_SIZE = 1024 * 1024 + LINK_REGEX = re.compile(r'') + RETRY_URL_REGEX = re.compile(r'"retry_url":"(?P[^"]*)"') + FORMATS = { + "FLAC": "flac", + "V0MP3": "mp3-v0", + "320MP3": "mp3-320", + "AAC": "aac-hi", + "Ogg": "vorbis", + "ALAC": "alac", + "WAV": "wav", + "AIFF": "aiff-lossless", + } + + def __init__( + self, + options: BCFreeDownloaderOptions, + config_dir: str, + download_history_file: str, + unzip: bool = True, + ): + self.options = options + self.config_dir = config_dir + self.download_history_file = download_history_file + self.downloaded: Set[str] = set() + self.mail_session = None + self.mail_album_data: Dict[str, BCFreeDownloaderAlbumData] = {} + self.unzip = unzip + self.session = None + self._init_email() + self._init_downloaded() + self._init_session() + + def _init_email(self): + if not self.options.email or self.options.email == "auto": + self.mail_session = secmail.Client(self.config_dir) + self.options.email = self.mail_session.random_email(1, "1secmail.com")[0] + + def _init_downloaded(self): + if self.download_history_file: + with open(self.download_history_file, "r") as f: + for line in f: + self.downloaded.add(line.strip()) + + def _init_session(self): + self.session = requests.Session() + + def _download_file( + self, + download_page_url: str, + format: str, + album_data: BCFreeDownloaderAlbumData = None, + ) -> str: + r = self.session.get(download_page_url) + r.raise_for_status() + soup = BeautifulSoup(r.text, "html.parser") + album_url = soup.find("div", class_="download-artwork").find("a").attrs["href"] + if album_data is None: + album_data = self.mail_album_data[album_url] + + data = json.loads(soup.find("div", {"id": "pagedata"}).attrs["data-blob"]) + download_url = data["digital_items"][0]["downloads"][self.FORMATS[format]][ + "url" + ] + + def download(download_url: str) -> str: + with self.session.get(download_url, stream=True) as r: + r.raise_for_status() + size = int(r.headers["content-length"]) + name = pyrfc6266.requests_response_to_filename(r) + file_name = os.path.join(self.options.dir, name) + with tqdm(total=size, unit="iB", unit_scale=True) as pbar: + with open(file_name, "wb") as f: + for chunk in r.iter_content(chunk_size=self.CHUNK_SIZE): + f.write(chunk) + pbar.update(len(chunk)) + return file_name + + try: + file_name = download(download_url) + except: + statdownload_url = download_url.replace("/download/", "/statdownload/") + with self.session.get(statdownload_url) as r: + r.raise_for_status() + download_url = self.RETRY_URL_REGEX.search(r.text).group("retry_url") + file_name = download(download_url) + + logger.info(f"Downloaded {file_name}") + + if file_name.endswith("zip") and self.unzip: + # Unzip archive + dir_name = file_name[:-4] + with zipfile.ZipFile(file_name, "r") as f: + f.extractall(dir_name) + logger.info(f"Unzipped to {dir_name}. Use --no-unzip to prevent this") + os.remove(file_name) + files = glob.glob(os.path.join(dir_name, "*")) + else: + files = [file_name] + # Tag downloaded audio files with url & comment + logger.info("Setting tags...") + for file in files: + f = mutagen.File(file) + if f is None: + continue + f["website"] = album_url + if album_data.tags: + f["genre"] = album_data.tags + comment = "" + if album_data.about: + comment += album_data.about + if album_data.about and album_data.credits: + comment += "\n\n" + if album_data.credits: + comment += album_data.credits + f["comment"] = comment + f.save() + # successfully downloaded file, add to download history + self.downloaded.add(album_url) + if self.download_history_file: + with open(self.download_history_file, "a") as f: + f.write(f"{album_url}\n") + + return album_url + + @staticmethod + def _get_album_data_from_soup(soup: BeautifulSoup) -> BCFreeDownloaderAlbumData: + album_data = BCFreeDownloaderAlbumData() + album_data.about = soup.find("div", class_="tralbum-about").get_text("\n") + album_data.credits = soup.find("div", class_="tralbum-credits").get_text("\n") + tags = [tag.get_text() for tag in soup.find_all("a", class_="tag")] + album_data.tags = ",".join(sorted(tags)) + return album_data + + def download_album(self, url: str, force: bool = False): + # Remove url params + url = urlsplit(url).geturl() + if url in self.downloaded and not force: + raise BCFreeDownloadError( + f"{url} already downloaded. To download anyways, use option --force" + ) + r = self.session.get(url) + r.raise_for_status() + soup = BeautifulSoup(r.text, "html.parser") + album_data = self._get_album_data_from_soup(soup) + + logger.debug(f"Album data: {album_data}") + + tralbum_data = soup.find("script", {"data-tralbum": True}).attrs["data-tralbum"] + tralbum_data = json.loads(tralbum_data) + + if tralbum_data["current"]["minimum_price"] == 0: + if tralbum_data["current"]["require_email"]: + logger.info(f"{url} requires email") + email_post_url = urljoin(url, "/email_download") + r = self.session.post( + email_post_url, + data={ + "encoding_name": "none", + "item_id": tralbum_data["current"]["id"], + "item_type": tralbum_data["current"]["type"], + "address": self.options.email, + "country": self.options.country, + "postcode": self.options.zipcode, + }, + ) + r.raise_for_status() + r = r.json() + if not r["ok"]: + raise ValueError(f"Bad response when sending email address: {r}") + self.mail_album_data[url] = album_data + else: + logger.info(f"{url} does not require email") + self._download_file( + tralbum_data["freeDownloadPage"], self.options.format, album_data + ) + else: + raise BCFreeDownloadError(f"{url} is not free") + + def download_label(self, url: str, force: bool = False): + r = self.session.get(url) + r.raise_for_status() + soup = BeautifulSoup(r.text) + for album_title in soup.find_all("p", class_="title"): + album_link = album_title.parent.attrs["href"] + logger.info(f"Downloading {album_link}") + try: + self.download_album(album_link, force) + except BCFreeDownloadError as ex: + logger.info(ex) + + def wait_for_email_downloads(self): + checked_ids = set() + while (expected_emails := len(self.mail_album_data)) > 0: + logger.info(f"Waiting for {expected_emails} emails from Bandcamp...") + time.sleep(5) + for email in self.mail_session.get_inbox(self.options.email): + if email.id not in checked_ids: + checked_ids.add(email.id) + if ( + email.from_address.endswith("@email.bandcamp.com") + and "download" in email.subject + ): + logger.info(f'Received email "{email.subject}"') + email = self.mail_session.get_message( + self.options.email, email.id + ) + match = self.LINK_REGEX.search(email.html_body) + if match: + download_url = match.group("url") + album_url = self._download_file( + download_url, self.options.format + ) + self.mail_album_data.pop(album_url) + + +class BCFreeDownloaderConfig: + def __init__(self, config_path: str): + self.config_path = config_path + self.parser = ConfigParser() + self.parser.read(config_path) + atexit.register(self.save) + + def get(self, key): + return self.parser["free-bandcamp-downloader"].get(key, None) + + def set(self, key, 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 get_config_dir(): + 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" + ) + return config_dir + + +def get_data_dir(): + 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" + ) + return data_dir + + +def get_config(data_dir: str, config_dir: str): + download_history_file = os.path.join(data_dir, "downloaded.txt") + default_config = f"""[free-bandcamp-downloader] + country = United States + zipcode = 00000 + email = auto + format = FLAC + dir = . + download_history_file = {download_history_file}""" + config_file = os.path.join(config_dir, "free-bandcamp-downloader.cfg") + if not os.path.exists(config_file): + if not os.path.exists(config_dir): + os.makedirs(config_dir) + with open(config_file, "w") as f: + f.write(default_config) + if not os.path.exists(download_history_file): + if not os.path.exists(data_dir): + os.makedirs(data_dir) + with open(download_history_file, "w") as f: + pass + config = BCFreeDownloaderConfig(config_file) + return config + + +def main(): + data_dir = get_data_dir() + config_dir = get_config_dir() + config = get_config(data_dir, config_dir) + options = BCFreeDownloaderOptions() + arguments = docopt(__doc__, version=__version__) + if arguments["-a"] or arguments["-l"] or arguments["setdefault"]: + # set options + for field in dataclasses.fields(options): + option = field.name + arg = f"--{option}" + if arguments[arg]: + setattr(options, option, arguments[arg][0]) + else: + setattr(options, option, config.get(option)) + if not getattr(options, option): + logger.error( + f'{option} is not set, use "bcdl-free setdefault {arg} <{option}>"' + ) + sys.exit(1) + if options.format not in BCFreeDownloader.FORMATS: + logger.error( + f'{options["format"]} is not a valid format. See "bcdl-free -h" for valid formats' + ) + sys.exit(1) + if arguments["-a"] or arguments["-l"]: + # init downloader + downloader = BCFreeDownloader( + options, + config_dir, + config.get("download_history_file"), + not arguments["--no-unzip"], + ) + if arguments["-a"]: + downloader.download_album(arguments["-a"], arguments["--force"]) + elif arguments["-l"]: + downloader.download_label(arguments["-l"], arguments["--force"]) + # finish up downloading + downloader.wait_for_email_downloads() + elif arguments["setdefault"]: + # write arguments to config + for field in dataclasses.fields(options): + option = field.name + arg = f"--{option}" + if arguments[arg]: + config.set(option, arguments[arg][0]) + elif arguments["defaults"]: + print(str(config)) + elif arguments["clear"]: + with open(config.get("download_history_file"), "w"): + pass + + +if __name__ == "__main__": + main() diff --git a/free_bandcamp_downloader/main.py b/free_bandcamp_downloader/main.py deleted file mode 100644 index fa17d94..0000000 --- a/free_bandcamp_downloader/main.py +++ /dev/null @@ -1,390 +0,0 @@ -"""Download free albums and tracks from Bandcamp -Usage: - bcdl-free (-a | -l )[--force][--no-unzip][-d | --dir ][-e | --email ] - [-z | --zipcode ][-c | --country ][-f | --format ] - bcdl-free setdefault [-d | --dir ][-e | --email ][-z | --zipcode ] - [-c | --country ][-f | --format ] - bcdl-free defaults - bcdl-free clear - bcdl-free (-h | --help) - bcdl-free --version -Options: - -h --help Show this screen - --version Show version - -a Download the album at URL - -l Download all free albums of the label at URL - --force Download even if album has been downloaded before - --no-unzip Don't unzip downloaded albums - setdefault Set default options - defaults List the default options - clear Clear download history - -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 -Formats: - - FLAC - - V0MP3 - - 320MP3 - - AAC - - Ogg - - ALAC - - WAV - - AIFF -""" - -import atexit -import glob -import os -import re -import sys -import time -import urllib.request -import zipfile -from urllib.parse import urlsplit - -import bs4 -import mutagen -from bs4 import BeautifulSoup, SoupStrainer -from docopt import docopt -from guerrillamail import GuerrillaMailSession -from selenium import webdriver -from selenium.webdriver.firefox.options import Options -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import Select -from selenium.webdriver.support.wait import WebDriverWait - -from free_bandcamp_downloader import __version__, config, logger - -# Global variables -arguments = None -mail_session = None -expected_emails = 0 -mail_album_data = {} -downloaded = set() -options = { - 'country': None, - 'zipcode': None, - 'email': None, - 'format': None, - 'dir': None -} - -# Constants - -link_regex = re.compile('') - -xpath = { - 'buy': '//*[@class="ft compound-button main-button"]/button[@class="download-link buy-link"]', - 'price': '//input[@id="userPrice"]', - 'download-nyp': '//a[@class="download-panel-free-download-link"]', - 'checkout': '//button[@class="download-panel-checkout-button" and not(ancestor::div[contains(@style,"display:none")])]', - 'email': '//input[@id="fan_email_address"]', - 'country': '//select[@id="fan_email_country"]', - 'zipcode': '//input[@id="fan_email_postalcode"]', - 'preparing': '//span[@class="preparing-title"]', - 'formats': '//select[@id="format-type"]', - 'download': '//div[@class="download-format-tmp"]/a[1]', - 'albums': '//a[./p[@class="title"]]', - 'album-link': '//div[@class="download-artwork"]/a', - 'album-tags': '//div[contains(@class, "tralbum-tags-nu")]', - 'album-credits': '//div[@class="tralbumData tralbum-credits"]', - 'album-about': '//div[@class="tralbumData tralbum-about"]', -} - -formats = { - 'FLAC': 'flac', - 'V0MP3': 'mp3-v0', - '320MP3': 'mp3-320', - 'AAC': 'aac-hi', - 'Ogg': 'vorbis', - 'ALAC': 'alac', - 'WAV': 'wav', - 'AIFF': 'aiff-lossless' -} - - -def wait(): - time.sleep(1) - - -def init_email(): - global mail_session - if not mail_session and (not options['email'] or options['email'] == 'auto'): - mail_session = GuerrillaMailSession() - options['email'] = mail_session.get_session_state()['email_address'] - - -def get_driver(): - options = Options() - options.add_argument('--headless') - driver = webdriver.Firefox( - options=options, service_log_path=os.devnull) - driver.implicitly_wait(10) - return driver - - -def init_downloaded(): - with open(config.get('download_history_file'), 'r') as f: - for line in f: - downloaded.add(line.strip()) - - -def download_file(driver, album_data=None): - logger.info('On download page') - page_url = driver.find_element("xpath", - xpath['album-link']).get_attribute('href') - page_url = 'https://' + BASE_URL + urlsplit(page_url).path - if album_data is None: - album_data = mail_album_data[page_url] - driver.find_element("xpath", xpath['formats']).click() - wait() - driver.find_element("xpath", - f'//option[@value="{formats[options["format"]]}"]').click() - logger.info(f'Set format to {formats[options["format"]]}') - wait() - button = driver.find_element("xpath", xpath['download']) - WebDriverWait(driver, 60).until(EC.visibility_of(button)) - url = button.get_attribute('href') - response = urllib.request.urlopen(url) - name = os.path.basename( - response.headers.get_filename().encode('latin-1').decode('utf-8')) - length = int(response.getheader('content-length')) - block_size = length // 10 - file_name = os.path.join(options['dir'], name) - with open(file_name, 'wb') as f: - size = 0 - while True: - buf = response.read(block_size) - if not buf: - break - f.write(buf) - size += len(buf) - logger.info(f'Downloading {name}, {size / length * 100: .1f}%') - response.close() - logger.info(f'Downloaded {file_name}') - if file_name.endswith('zip') and not arguments['--no-unzip']: - # Unzip archive - dir_name = file_name[:-4] - with zipfile.ZipFile(file_name, 'r') as f: - f.extractall(dir_name) - logger.info(f'Unzipped to {dir_name}. Use --no-unzip to prevent this') - os.remove(file_name) - files = glob.glob(os.path.join(dir_name, "*")) - else: - files = [file_name] - # Tag downloaded audio files with url & comment - logger.info("Setting tags...") - for file in files: - try: - f = mutagen.File(file) - if f is None: - continue - f['website'] = page_url - if album_data['tags']: - f['genre'] = album_data['tags'] - if album_data['about'] or album_data['credits']: - comment = '' - if album_data['about']: - comment += album_data['about'] - if album_data['about'] and album_data['credits']: - comment += '\n\n' - if album_data['credits']: - comment += album_data['credits'] - f['comment'] = comment - f.save() - except Exception as e: - logger.info(f"Could not tag {file} - {e.__class__}: {e}") - # successfully downloaded file, add to download history - downloaded.add(page_url) - with open(config.get('download_history_file'), 'a') as f: - f.write(f'{page_url}\n') - - -def download_files(driver, urls): - for url in urls: - driver.get(url) - download_file(driver) - - -def get_text(text): - text = ''.join(line.strip() for line in text.split('\n')) - text = text.replace('
', '\n') - return BeautifulSoup(text, features='html.parser').get_text() - - -def download_album(driver, url): - # Remove url params - url = urlsplit(url).geturl() - if url in downloaded and not arguments['--force']: - logger.error( - f'{url} already downloaded. To download anyways, use option --force') - return f'{url} already downloaded. To download anyways, use option --force' - driver.get(url) - wait() - try: - # Get album data - album_data = { - 'about': None, - 'credits': None, - 'tags': None - } - try: - s = driver.find_element("xpath", - xpath['album-about']).get_attribute('innerHTML') - s = get_text(s) - album_data['about'] = s - except Exception as e: - logger.info(f"Could not get album about - {e.__class__}: {e}") - try: - s = driver.find_element("xpath", - xpath['album-credits']).get_attribute('innerHTML') - s = get_text(s) - album_data['credits'] = s - except Exception as e: - logger.info(f"Could not get album credits - {e.__class__}: {e}") - try: - s = driver.find_element("xpath", - xpath['album-tags']).get_attribute('innerHTML') - tags = {a.text for a in BeautifulSoup( - s, features='html.parser', parse_only=SoupStrainer('a'))} - album_data['tags'] = ','.join(sorted(tags)) - except Exception as e: - logger.info(f"Could not get album tags - {e.__class__}: {e}") - - logger.info(f"Album data: {album_data}") - - button = driver.find_element("xpath", xpath['buy']) - if button.text == 'Free Download': - logger.info(f'{url} is Free Download') - button.click() - wait() - return download_file(driver, album_data) - else: - # name your price download - logger.info(f'{url} is not Free Download') - button.click() - wait() - price_input = driver.find_element("xpath", xpath['price']) - price_input.click() - price_input.send_keys('0') - wait() - try: - driver.find_element("xpath", xpath['download-nyp']).click() - except: - logger.error(f'{url} is not free') - return f'{url} is not free' - checkout = driver.find_element("xpath", xpath['checkout']) - if checkout.text == 'Download Now': - checkout.click() - wait() - return download_file(driver, album_data) - else: - init_email() - logger.info(f'{url} requires email') - # fill out info - driver.find_element("xpath", - xpath['email']).send_keys(options['email']) - wait() - driver.find_element("xpath", - xpath['zipcode']).send_keys(options['zipcode']) - wait() - Select(driver.find_element("xpath", - xpath['country'])).select_by_visible_text(options['country']) - wait() - checkout.click() - global expected_emails - expected_emails += 1 - mail_album_data[url] = album_data - except Exception as e: - logger.error(f"Error downloading {url} - {e.__class__}: {e}") - return e - - -def download_albums(driver, urls): - for link in urls: - logger.info(f'Downloading {link}') - download_album(driver, link) - - -def download_label(driver, url): - driver.get(url) - global BASE_URL - BASE_URL = urlsplit(url).netloc - links = [] - for album in driver.find_elements("xpath", xpath['albums']): - links.append(album.get_attribute('href')) - download_albums(driver, links) - - -def main(): - global arguments - arguments = docopt(__doc__, version=__version__) - if arguments['-a'] or arguments['-l']: - # set options - for option in options: - arg = f'--{option}' - if arguments[arg]: - options[option] = arguments[arg][0] - else: - options[option] = config.get(option) - if not options[option]: - logger.error( - f'{option} is not set, use "bcdl-free setdefault {arg} <{option}>"') - sys.exit(1) - if options['format'] not in formats: - logger.error( - f'{options["format"]} is not a valid format. See "bcdl-free -h" for valid formats') - sys.exit(1) - init_downloaded() - driver = get_driver() - try: - if arguments['-a']: - err = download_album(driver, arguments['-a']) - if err: - sys.exit(1) - elif arguments['-l']: - download_label(driver, arguments['-l']) - elif arguments['setdefault']: - # write arguments to config - for option in options: - arg = f'--{option}' - if arguments[arg]: - config.set(option, arguments[arg][0]) - elif arguments['defaults']: - print(str(config)) - elif arguments['clear']: - with open(config.get('download_history_file'), 'w'): - pass - # download emailed albums - checked_ids = set() - album_urls = set() - global expected_emails - logger.info(f'Waiting for {expected_emails} emails from bandcamp') - while expected_emails > 0: - time.sleep(10) - try: - for email in mail_session.get_email_list(): - if email.guid not in checked_ids: - checked_ids.add(email.guid) - if email.sender == 'noreply@bandcamp.com' and 'download' in email.subject: - logger.info(f'Received email "{email.subject}"') - email = mail_session.get_email(email.guid) - match = link_regex.search(email.body) - if match: - download_url = match.group('url') - album_urls.add(download_url) - expected_emails -= 1 - except Exception as e: - logger.error(e) - logger.info(f'Downloading {len(album_urls)} albums...') - download_files(driver, album_urls) - except: - raise - finally: - driver.quit() - - -if __name__ == '__main__': - main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a01eec0 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,389 @@ +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. + +[[package]] +name = "1secmail" +version = "1.1.0" +description = "📧 Simple and intuitive, yet full featured API wrapper for www.1secmail.com, supporting both synchronous and asynchronous operations." +optional = false +python-versions = "*" +files = [ + {file = "1secMail-1.1.0-py3-none-any.whl", hash = "sha256:0277277ad17e7e8ddd42fb0e4be31e03b1c4543776d90aa78ae80c4ea41f4a19"}, + {file = "1secMail-1.1.0.tar.gz", hash = "sha256:7acd1573e18079880df99d74a077e78bbd1b0c7c468ff68e3fafc57572a7384a"}, +] + +[package.dependencies] +httpx = ">=0.17.1" + +[[package]] +name = "anyio" +version = "4.1.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, + {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "certifi" +version = "2023.11.17" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "docopt" +version = "0.6.2" +description = "Pythonic argument parser, that will make you smile" +optional = false +python-versions = "*" +files = [ + {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[[package]] +name = "httpx" +version = "0.25.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "mutagen" +version = "1.47.0" +description = "read and write audio tags for many formats" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"}, + {file = "mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99"}, +] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyrfc6266" +version = "1.0.2" +description = "RFC6266 implementation in Python" +optional = false +python-versions = "*" +files = [ + {file = "pyrfc6266-1.0.2-py3-none-any.whl", hash = "sha256:0532307f319566f337dba97577dfaefe493c3e0c40ab211449ba4566fc2cf73d"}, + {file = "pyrfc6266-1.0.2.tar.gz", hash = "sha256:3c41616b6a1f2e9a26df7f005fbaa634f960121769ccc4445acfb404e9f8fd4c"}, +] + +[package.dependencies] +pyparsing = ">=3.0.7,<3.1.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "110161d32164c3a61a75e6cccd2c99e1d95130f9f810290c30dab899212cc577" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0b69bdd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "free-bandcamp-downloader" +version = "0.1.0" +description = "Download free and name-your-price albums from Bandcamp" +authors = ["7x11x13 "] +license = "MIT" +readme = "README.md" + +[tool.poetry.scripts] +bcdl-free = "free_bandcamp_downloader.__main__:main" + +[tool.poetry.dependencies] +python = "^3.8" +requests = "^2.31.0" +beautifulsoup4 = "^4.12.2" +tqdm = "^4.66.1" +pyrfc6266 = "^1.0.2" +mutagen = "^1.47.0" +docopt = "^0.6.2" +1secmail = "^1.1.0" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/setup.py b/setup.py deleted file mode 100644 index 5943dbf..0000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -from setuptools import find_packages, setup - -import free_bandcamp_downloader - -setup( - name='free-bandcamp-downloader', - version=free_bandcamp_downloader.__version__, - packages=find_packages(), - author='7x11x13', - install_requires=[ - 'selenium', - 'docopt', - 'python-guerrillamail', - 'mutagen', - 'beautifulsoup4' - ], - entry_points={ - 'console_scripts': [ - 'bcdl-free = free_bandcamp_downloader.main:main' - ] - } -) From f95cb7c18cad41eedc0489265d3b9c4200fefac0 Mon Sep 17 00:00:00 2001 From: 7x11x13 Date: Thu, 30 Nov 2023 18:01:59 -0500 Subject: [PATCH 2/2] Publish to PyPI --- .github/workflows/publish.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..078581c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,16 @@ +name: publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build and publish to pypi + uses: JRubics/poetry-publish@v1.16 + with: + pypi_token: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file