Housekeeping + Minor bugfixes
Fixed: #186, #188, #189 Cleaned up lingering `.format()`s and replaced with appropriate f-strings. Updated project structure to slightly more modern standards.master
parent
3019b26c46
commit
cf9d43fed2
|
@ -65,15 +65,15 @@ from bandcamp_dl.__init__ import __version__
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
arguments = docopt(__doc__, version='bandcamp-dl {}'.format(__version__))
|
arguments = docopt(__doc__, version=f'bandcamp-dl {__version__}')
|
||||||
|
|
||||||
if arguments['--debug']:
|
if arguments['--debug']:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
bandcamp = Bandcamp()
|
bandcamp = Bandcamp()
|
||||||
|
|
||||||
basedir = arguments['--base-dir'] or os.getcwd()
|
basedir = arguments['--base-dir'] or os.path.expanduser('~')
|
||||||
session_file = "{}/{}.not.finished".format(basedir, __version__)
|
session_file = f"{basedir}/{__version__}.not.finished"
|
||||||
|
|
||||||
if os.path.isfile(session_file) and arguments['URL'] is None:
|
if os.path.isfile(session_file) and arguments['URL'] is None:
|
||||||
with open(session_file, "r") as f:
|
with open(session_file, "r") as f:
|
||||||
|
@ -92,24 +92,25 @@ def main():
|
||||||
exit()
|
exit()
|
||||||
else:
|
else:
|
||||||
urls = arguments['URL']
|
urls = arguments['URL']
|
||||||
|
|
||||||
|
album_list = []
|
||||||
for url in urls:
|
for url in urls:
|
||||||
logging.debug("\n\tURL: {}".format(url))
|
logging.debug("\n\tURL: {}".format(url))
|
||||||
# url is now a list of URLs. So lets make an albumList and append each parsed album to it.
|
album_list.append(
|
||||||
albumList = []
|
bandcamp.parse(url, not arguments['--no-art'], arguments['--embed-lyrics'], arguments['--debug']))
|
||||||
for url in urls:
|
# url is now a list of URLs. So lets make an album_list and append each parsed album to it.
|
||||||
albumList.append(bandcamp.parse(url, not arguments['--no-art'], arguments['--embed-lyrics'], arguments['--debug']))
|
|
||||||
|
|
||||||
for album in albumList:
|
for album in album_list:
|
||||||
logging.debug(" Album data:\n\t{}".format(album))
|
logging.debug(" Album data:\n\t{}".format(album))
|
||||||
|
|
||||||
for album in albumList:
|
for album in album_list:
|
||||||
if arguments['--full-album'] and not album['full']:
|
if arguments['--full-album'] and not album['full']:
|
||||||
print("Full album not available. Skipping ", album['title'], " ...")
|
print("Full album not available. Skipping ", album['title'], " ...")
|
||||||
albumList.remove(album) # Remove not-full albums BUT continue with the rest of the albums.
|
album_list.remove(album) # Remove not-full albums BUT continue with the rest of the albums.
|
||||||
|
|
||||||
if arguments['URL'] or arguments['--artist']:
|
if arguments['URL'] or arguments['--artist']:
|
||||||
logging.debug("Preparing download process..")
|
logging.debug("Preparing download process..")
|
||||||
for album in albumList:
|
for album in album_list:
|
||||||
bandcamp_downloader = BandcampDownloader(arguments['--template'], basedir, arguments['--overwrite'],
|
bandcamp_downloader = BandcampDownloader(arguments['--template'], basedir, arguments['--overwrite'],
|
||||||
arguments['--embed-lyrics'], arguments['--group'],
|
arguments['--embed-lyrics'], arguments['--group'],
|
||||||
arguments['--embed-art'], arguments['--no-slugify'],
|
arguments['--embed-art'], arguments['--no-slugify'],
|
||||||
|
|
|
@ -12,10 +12,12 @@ from bandcamp_dl.__init__ import __version__
|
||||||
|
|
||||||
class Bandcamp:
|
class Bandcamp:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.headers = {'User-Agent': 'bandcamp-dl/{} (https://github.com/iheanyi/bandcamp-dl)'.format(__version__)}
|
self.headers = {'User-Agent': f'bandcamp-dl/{__version__} (https://github.com/iheanyi/bandcamp-dl)'}
|
||||||
|
self.soup = None
|
||||||
|
self.tracks = None
|
||||||
|
|
||||||
def parse(self, url: str, art: bool=True, lyrics: bool=False, debugging: bool=False) -> dict or None:
|
def parse(self, url: str, art: bool = True, lyrics: bool = False, debugging: bool = False) -> dict or None:
|
||||||
"""Requests the page, cherry picks album info
|
"""Requests the page, cherry-picks album info
|
||||||
|
|
||||||
:param url: album/track url
|
:param url: album/track url
|
||||||
:param art: if True download album art
|
:param art: if True download album art
|
||||||
|
@ -56,7 +58,7 @@ class Bandcamp:
|
||||||
album_title = page_json['trackinfo'][0]['title']
|
album_title = page_json['trackinfo'][0]['title']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
label = page_json['item_sellers']['{}'.format(page_json['current']['selling_band_id'])]['name']
|
label = page_json['item_sellers'][f'{page_json["current"]["selling_band_id"]}']['name']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
label = None
|
label = None
|
||||||
|
|
||||||
|
@ -71,10 +73,14 @@ class Bandcamp:
|
||||||
"url": url
|
"url": url
|
||||||
}
|
}
|
||||||
|
|
||||||
artist_url = page_json['url'].rpartition('/album/')[0]
|
if "track" in page_json['url']:
|
||||||
|
artist_url = page_json['url'].rpartition('/track/')[0]
|
||||||
|
else:
|
||||||
|
artist_url = page_json['url'].rpartition('/album/')[0]
|
||||||
|
|
||||||
for track in self.tracks:
|
for track in self.tracks:
|
||||||
if lyrics:
|
if lyrics:
|
||||||
track['lyrics'] = self.get_track_lyrics("{}{}#lyrics".format(artist_url, track['title_link']))
|
track['lyrics'] = self.get_track_lyrics(f"{artist_url}{track['title_link']}#lyrics")
|
||||||
if track['file'] is not None:
|
if track['file'] is not None:
|
||||||
track = self.get_track_metadata(track)
|
track = self.get_track_metadata(track)
|
||||||
album['tracks'].append(track)
|
album['tracks'].append(track)
|
||||||
|
@ -84,7 +90,7 @@ class Bandcamp:
|
||||||
album['art'] = self.get_album_art()
|
album['art'] = self.get_album_art()
|
||||||
|
|
||||||
logging.debug(" Album generated..")
|
logging.debug(" Album generated..")
|
||||||
logging.debug(" Album URL: {}".format(album['url']))
|
logging.debug(f" Album URL: {album['url']}")
|
||||||
|
|
||||||
return album
|
return album
|
||||||
|
|
||||||
|
@ -153,7 +159,7 @@ class Bandcamp:
|
||||||
:param page_type: Type of page album/track
|
:param page_type: Type of page album/track
|
||||||
:return: url as str
|
:return: url as str
|
||||||
"""
|
"""
|
||||||
return "http://{0}.bandcamp.com/{1}/{2}".format(artist, page_type, slug)
|
return f"http://{artist}.bandcamp.com/{page_type}/{slug}"
|
||||||
|
|
||||||
def get_album_art(self) -> str:
|
def get_album_art(self) -> str:
|
||||||
"""Find and retrieve album art url from page
|
"""Find and retrieve album art url from page
|
||||||
|
|
|
@ -29,7 +29,7 @@ class BandcampDownloader:
|
||||||
:param directory: download location
|
:param directory: download location
|
||||||
:param overwrite: if True overwrite existing files
|
:param overwrite: if True overwrite existing files
|
||||||
"""
|
"""
|
||||||
self.headers = {'User-Agent': 'bandcamp-dl/{} (https://github.com/iheanyi/bandcamp-dl)'.format(__version__)}
|
self.headers = {'User-Agent': f'bandcamp-dl/{__version__} (https://github.com/iheanyi/bandcamp-dl)'}
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
if type(urls) is str:
|
if type(urls) is str:
|
||||||
|
@ -102,10 +102,12 @@ class BandcampDownloader:
|
||||||
else:
|
else:
|
||||||
path = path.replace("%{track}", str(track['track']).zfill(2))
|
path = path.replace("%{track}", str(track['track']).zfill(2))
|
||||||
|
|
||||||
path = u"{0}/{1}.{2}".format(self.directory, path, "mp3")
|
# Double check that the old issue in Python 2 with unicode strings isn't a problem with f-strings
|
||||||
|
# Otherwise find an alternative to u'STRING'
|
||||||
|
path = f"{self.directory}/{path}.mp3"
|
||||||
|
|
||||||
logging.debug(" filepath/trackname generated..")
|
logging.debug(" filepath/trackname generated..")
|
||||||
logging.debug("\n\tPath: {}".format(path))
|
logging.debug(f"\n\tPath: {path}")
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -116,7 +118,7 @@ class BandcampDownloader:
|
||||||
:return: directory path
|
:return: directory path
|
||||||
"""
|
"""
|
||||||
directory = os.path.dirname(filename)
|
directory = os.path.dirname(filename)
|
||||||
logging.debug(" Directory:\n\t{}".format(directory))
|
logging.debug(f" Directory:\n\t{directory}")
|
||||||
logging.debug(" Directory doesn't exist, creating..")
|
logging.debug(" Directory doesn't exist, creating..")
|
||||||
if not os.path.exists(directory):
|
if not os.path.exists(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
|
@ -144,11 +146,12 @@ class BandcampDownloader:
|
||||||
self.num_tracks = len(album['tracks'])
|
self.num_tracks = len(album['tracks'])
|
||||||
self.track_num = track_index + 1
|
self.track_num = track_index + 1
|
||||||
|
|
||||||
filepath = self.template_to_path(track_meta, self.ascii_only, self.ok_chars, self.space_char, self.keep_space, self.keep_upper) + ".tmp"
|
filepath = self.template_to_path(track_meta, self.ascii_only, self.ok_chars, self.space_char,
|
||||||
|
self.keep_space, self.keep_upper) + ".tmp"
|
||||||
filename = filepath.rsplit('/', 1)[1]
|
filename = filepath.rsplit('/', 1)[1]
|
||||||
dirname = self.create_directory(filepath)
|
dirname = self.create_directory(filepath)
|
||||||
|
|
||||||
logging.debug(" Current file:\n\t{}".format(filepath))
|
logging.debug(f" Current file:\n\t{filepath}")
|
||||||
|
|
||||||
if album['art'] and not os.path.exists(dirname + "/cover.jpg"):
|
if album['art'] and not os.path.exists(dirname + "/cover.jpg"):
|
||||||
try:
|
try:
|
||||||
|
@ -180,7 +183,7 @@ class BandcampDownloader:
|
||||||
# break out of the try/except and move on to the next file
|
# break out of the try/except and move on to the next file
|
||||||
break
|
break
|
||||||
elif os.path.exists(filepath[:-4]) and self.overwrite is not True:
|
elif os.path.exists(filepath[:-4]) and self.overwrite is not True:
|
||||||
print("File: {} already exists and is complete, skipping..".format(filename[:-4]))
|
print(f"File: {filename[:-4]} already exists and is complete, skipping..")
|
||||||
skip = True
|
skip = True
|
||||||
break
|
break
|
||||||
with open(filepath, "wb") as f:
|
with open(filepath, "wb") as f:
|
||||||
|
@ -194,13 +197,11 @@ class BandcampDownloader:
|
||||||
if not self.debugging:
|
if not self.debugging:
|
||||||
done = int(50 * dl / file_length)
|
done = int(50 * dl / file_length)
|
||||||
print_clean(
|
print_clean(
|
||||||
"\r({}/{}) [{}{}] :: Downloading: {}".format(self.track_num, self.num_tracks,
|
f'\r({self.track_num}/{self.num_tracks}) [{"=" * done}{" " * (50 - done)}] :: Downloading: {filename[:-8]}')
|
||||||
"=" * done, " " * (50 - done),
|
|
||||||
filename[:-8]))
|
|
||||||
local_size = os.path.getsize(filepath)
|
local_size = os.path.getsize(filepath)
|
||||||
# if the local filesize before encoding doesn't match the remote filesize redownload
|
# if the local filesize before encoding doesn't match the remote filesize redownload
|
||||||
if local_size != file_length and attempts != 3:
|
if local_size != file_length and attempts != 3:
|
||||||
print("{} is incomplete, retrying..".format(filename))
|
print(f"{filename} is incomplete, retrying..")
|
||||||
continue
|
continue
|
||||||
# if the maximum number of retry attempts is reached give up and move on
|
# if the maximum number of retry attempts is reached give up and move on
|
||||||
elif attempts == 3:
|
elif attempts == 3:
|
||||||
|
@ -218,8 +219,8 @@ class BandcampDownloader:
|
||||||
if skip is False:
|
if skip is False:
|
||||||
self.write_id3_tags(filepath, track_meta)
|
self.write_id3_tags(filepath, track_meta)
|
||||||
|
|
||||||
if os.path.isfile("{}/{}.not.finished".format(self.directory, __version__)):
|
if os.path.isfile(f"{self.directory}/{__version__}.not.finished"):
|
||||||
os.remove("{}/{}.not.finished".format(self.directory, __version__))
|
os.remove(f"{self.directory}/{__version__}.not.finished")
|
||||||
|
|
||||||
# Remove album art image as it is embedded
|
# Remove album art image as it is embedded
|
||||||
if self.embed_art:
|
if self.embed_art:
|
||||||
|
@ -238,7 +239,7 @@ class BandcampDownloader:
|
||||||
filename = filepath.rsplit('/', 1)[1][:-8]
|
filename = filepath.rsplit('/', 1)[1][:-8]
|
||||||
|
|
||||||
if not self.debugging:
|
if not self.debugging:
|
||||||
print_clean("\r({}/{}) [{}] :: Encoding: {}".format(self.track_num, self.num_tracks, "=" * 50, filename))
|
print_clean(f'\r({self.track_num}/{self.num_tracks}) [{"=" * 50}] :: Encoding: {filename}')
|
||||||
|
|
||||||
audio = MP3(filepath)
|
audio = MP3(filepath)
|
||||||
audio.delete()
|
audio.delete()
|
||||||
|
@ -267,7 +268,7 @@ class BandcampDownloader:
|
||||||
audio.save()
|
audio.save()
|
||||||
|
|
||||||
logging.debug(" Encoding process finished..")
|
logging.debug(" Encoding process finished..")
|
||||||
logging.debug(" Renaming:\n\t{} -to-> {}".format(filepath, filepath[:-4]))
|
logging.debug(f" Renaming:\n\t{filepath} -to-> {filepath[:-4]}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.rename(filepath, filepath[:-4])
|
os.rename(filepath, filepath[:-4])
|
||||||
|
@ -276,4 +277,4 @@ class BandcampDownloader:
|
||||||
os.rename(filepath, filepath[:-4])
|
os.rename(filepath, filepath[:-4])
|
||||||
|
|
||||||
if not self.debugging:
|
if not self.debugging:
|
||||||
print_clean("\r({}/{}) [{}] :: Finished: {}".format(self.track_num, self.num_tracks, "=" * 50, filename))
|
print_clean(f'\r({self.track_num}/{self.num_tracks}) [{"=" * 50}] :: Finished: {filename}')
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import demjson
|
import demjson3
|
||||||
|
|
||||||
|
|
||||||
class BandcampJSON:
|
class BandcampJSON:
|
||||||
def __init__(self, body, debugging: bool=False):
|
def __init__(self, body, debugging: bool = False):
|
||||||
self.body = body
|
self.body = body
|
||||||
self.json_data = []
|
self.json_data = []
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ class BandcampJSON:
|
||||||
"""Convert JavaScript dictionary to JSON"""
|
"""Convert JavaScript dictionary to JSON"""
|
||||||
logging.debug(" Converting JS to JSON..")
|
logging.debug(" Converting JS to JSON..")
|
||||||
# Decode with demjson first to reformat keys and lists
|
# Decode with demjson first to reformat keys and lists
|
||||||
decoded_js = demjson.decode(js_data)
|
decoded_js = demjson3.decode(js_data)
|
||||||
# Encode to make valid JSON, add to list of JSON strings
|
# Encode to make valid JSON, add to list of JSON strings
|
||||||
return demjson.encode(decoded_js)
|
encoded_json = demjson3.encode(decoded_js)
|
||||||
|
return demjson3.encode(decoded_js)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
beautifulsoup4==4.6.0
|
beautifulsoup4==4.10.0
|
||||||
demjson==2.2.4
|
demjson3==3.0.5
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
mutagen==1.38
|
mutagen==1.45.1
|
||||||
requests==2.18.4
|
requests==2.26.0
|
||||||
unicode-slugify==0.1.3
|
unicode-slugify==0.1.5
|
||||||
mock==2.0.0
|
mock==4.0.3
|
||||||
chardet==3.0.4
|
chardet==4.0.0
|
||||||
|
|
|
@ -3,5 +3,4 @@ import shutil
|
||||||
|
|
||||||
def print_clean(msg):
|
def print_clean(msg):
|
||||||
terminal_size = shutil.get_terminal_size()
|
terminal_size = shutil.get_terminal_size()
|
||||||
msg_length = len(msg)
|
print(f'{msg}{" " * (int(terminal_size[0]) - len(msg))}', end='')
|
||||||
print("{}{}".format(msg, " " * (int(terminal_size[0]) - msg_length)), end='')
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ def parse_headers(fp, _class=http.client.HTTPMessage):
|
||||||
raise http.client.LineTooLong("header line")
|
raise http.client.LineTooLong("header line")
|
||||||
headers.append(line)
|
headers.append(line)
|
||||||
if len(headers) > http.client._MAXHEADERS:
|
if len(headers) > http.client._MAXHEADERS:
|
||||||
raise HTTPException("got more than {} headers".format(http.client._MAXHEADERS))
|
raise HTTPException(f"got more than {http.client._MAXHEADERS} headers")
|
||||||
if line in (b'\r\n', b'\n', b''):
|
if line in (b'\r\n', b'\n', b''):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=43.0.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
|
@ -1,10 +1,10 @@
|
||||||
--index-url https://pypi.python.org/simple/
|
--index-url https://pypi.python.org/simple/
|
||||||
|
|
||||||
beautifulsoup4==4.9.3
|
beautifulsoup4==4.10.0
|
||||||
demjson==2.2.4
|
demjson3==3.0.5
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
mutagen==1.45.1
|
mutagen==1.45.1
|
||||||
requests==2.25.1
|
requests==2.26.0
|
||||||
unicode-slugify==0.1.3
|
unicode-slugify==0.1.5
|
||||||
mock==4.0.3
|
mock==4.0.3
|
||||||
chardet==4.0.0
|
chardet==4.0.0
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
universal = 0
|
universal = 0
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
license_file = LICENSE
|
license_files = LICENSE
|
||||||
|
|
27
setup.py
27
setup.py
|
@ -1,20 +1,19 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from codecs import open
|
import pathlib
|
||||||
from os import path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
appversion = "0.0.10"
|
appversion = "0.0.11-dev"
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
here = pathlib.Path(__file__).parent.resolve()
|
||||||
|
|
||||||
with open(here + '/bandcamp_dl/__init__.py', 'w') as initpy:
|
with open(f'{here}/bandcamp_dl/__init__.py', 'w') as initpy:
|
||||||
initpy.write('__version__ = "{}"'.format(appversion))
|
initpy.write(f'__version__ = "{appversion}"')
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='bandcamp-downloader',
|
name='bandcamp-downloader',
|
||||||
version=appversion,
|
version=appversion,
|
||||||
description='bandcamp-dl downloads albums and tracks from Bandcamp for you',
|
description='bandcamp-dl downloads albums and tracks from Bandcamp for you',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open('README.rst').read(),
|
||||||
|
long_description_content_type='text/x-rst',
|
||||||
url='https://github.com/iheanyi/bandcamp-dl',
|
url='https://github.com/iheanyi/bandcamp-dl',
|
||||||
author='Iheanyi Ekechukwu',
|
author='Iheanyi Ekechukwu',
|
||||||
author_email='iekechukwu@gmail.com',
|
author_email='iekechukwu@gmail.com',
|
||||||
|
@ -28,13 +27,19 @@ setup(
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
],
|
],
|
||||||
keywords=['bandcamp', 'downloader', 'music', 'cli', 'albums', 'dl'],
|
keywords=['bandcamp', 'downloader', 'music', 'cli', 'albums', 'dl'],
|
||||||
packages=find_packages(exclude=['tests']),
|
packages=find_packages(exclude=['tests']),
|
||||||
python_requires='~=3.4',
|
python_requires='>=3.4',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'beautifulsoup4',
|
'beautifulsoup4',
|
||||||
'demjson',
|
'lxml',
|
||||||
|
'demjson3',
|
||||||
'docopt',
|
'docopt',
|
||||||
'mutagen',
|
'mutagen',
|
||||||
'requests',
|
'requests',
|
||||||
|
@ -53,4 +58,8 @@ setup(
|
||||||
'bandcamp-dl=bandcamp_dl.__main__:main',
|
'bandcamp-dl=bandcamp_dl.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
project_urls={
|
||||||
|
'Bug Reports': 'https://github.com/iheanyi/bandcamp-dl/issues',
|
||||||
|
'Source': 'https://github.com/iheanyi/bandcamp-dl',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue