This commit is contained in:
7x11x13
2023-11-30 17:59:28 -05:00
parent 6fb474be50
commit 29d0597d93
8 changed files with 840 additions and 485 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
env
*.egg-info
*.log
__pycache__
__pycache__
notes
-4
View File
@@ -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
+3 -68
View File
@@ -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")
+421
View File
@@ -0,0 +1,421 @@
"""Download free albums and tracks from Bandcamp
Usage:
bcdl-free (-a <URL> | -l <URL>)[--force][--no-unzip][-d | --dir <dir>][-e | --email <email>]
[-z | --zipcode <zipcode>][-c | --country <country>][-f | --format <format>]
bcdl-free setdefault [-d | --dir <dir>][-e | --email <email>][-z | --zipcode <zipcode>]
[-c | --country <country>][-f | --format <format>]
bcdl-free defaults
bcdl-free clear
bcdl-free (-h | --help)
bcdl-free --version
Options:
-h --help Show this screen
--version Show version
-a <URL> Download the album at URL
-l <URL> 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 <dir> Set download directory
-c --country <country> Set country
-z --zipcode <zipcode> Set zipcode
-e --email <email> Set email (set to 'auto' to automatically download from a disposable email)
-f --format <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'<a href="(?P<url>[^"]*)">')
RETRY_URL_REGEX = re.compile(r'"retry_url":"(?P<retry_url>[^"]*)"')
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()
-390
View File
@@ -1,390 +0,0 @@
"""Download free albums and tracks from Bandcamp
Usage:
bcdl-free (-a <URL> | -l <URL>)[--force][--no-unzip][-d | --dir <dir>][-e | --email <email>]
[-z | --zipcode <zipcode>][-c | --country <country>][-f | --format <format>]
bcdl-free setdefault [-d | --dir <dir>][-e | --email <email>][-z | --zipcode <zipcode>]
[-c | --country <country>][-f | --format <format>]
bcdl-free defaults
bcdl-free clear
bcdl-free (-h | --help)
bcdl-free --version
Options:
-h --help Show this screen
--version Show version
-a <URL> Download the album at URL
-l <URL> 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 <dir> Set download directory
-c --country <country> Set country
-z --zipcode <zipcode> Set zipcode
-e --email <email> Set email (set to 'auto' to automatically download from a disposable email)
-f --format <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('<a href="(?P<url>http[^"]*)">')
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('<br>', '\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()
Generated
+389
View File
@@ -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"
+25
View File
@@ -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 <x7x11x13@gmail.com>"]
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"
-22
View File
@@ -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'
]
}
)