Rewrite
This commit is contained in:
@@ -2,3 +2,4 @@ env
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
*.log
|
*.log
|
||||||
__pycache__
|
__pycache__
|
||||||
|
notes
|
||||||
@@ -5,10 +5,6 @@ and tags them with data from the Bandcamp page.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Requires Firefox and [geckodriver](https://github.com/mozilla/geckodriver/releases)
|
|
||||||
|
|
||||||
Make sure geckodriver is in your PATH environment variable
|
|
||||||
|
|
||||||
Install with pip
|
Install with pip
|
||||||
```
|
```
|
||||||
pip install git+https://github.com/7x11x13/free-bandcamp-downloader
|
pip install git+https://github.com/7x11x13/free-bandcamp-downloader
|
||||||
|
|||||||
@@ -1,73 +1,8 @@
|
|||||||
import atexit
|
import importlib.metadata
|
||||||
import logging
|
import logging
|
||||||
import os
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__version__ = 'v0.0.7'
|
__version__ = importlib.metadata.version("free-bandcamp-downloader")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user