0.0.8 Update

Added --track for downloading individual tracks/singles
Added --embed-art to forcibly embed album art if available
master
Anthony Forsberg 2017-04-15 09:30:34 -04:00
parent 9db2aba929
commit 12327e119b
7 changed files with 86 additions and 118 deletions

View File

@ -17,6 +17,7 @@ Version 0.0.6
- [Enhancement] Individual track downloads work now. - [Enhancement] Individual track downloads work now.
- [Bugfix] Fixed imports, now working when installed via pip. - [Bugfix] Fixed imports, now working when installed via pip.
- [Note] Last version to officially support Python 2.7.x - [Note] Last version to officially support Python 2.7.x
- [Bugfix] Fixed an encoding issue with accented characters in the filepath. (Thanks `oaubert <https://github.com/oaubert>`_)
Version 0.0.7 Version 0.0.7
------------- -------------
@ -28,3 +29,11 @@ Version 0.0.7
- [Dependency] Slimit is no longer required. - [Dependency] Slimit is no longer required.
- [Dependency] Ply is no longer required. - [Dependency] Ply is no longer required.
- [Dependency] demjson is now required. - [Dependency] demjson is now required.
- [Bugfix] Downloading singles is now fixed.
- [Bugfix] Monkey-patched Requests to fix compatability with Python versions before 3.6.
- [Enhancement] Added a --group option to insert a group tag (iTunes)
Version 0.0.8
-------------
- [Enhancement] --embed-art option to forcibly embed album art (if available)
- [Enhancement] --track option for downloading individual tracks and singles.

View File

@ -21,7 +21,7 @@ From Wheel
3. ``pip install <filename>.whl`` 3. ``pip install <filename>.whl``
[OSX] From Homebrew [OSX] From Homebrew
---------- -------------------
``brew install bandcamp-dl`` ``brew install bandcamp-dl``
@ -46,14 +46,18 @@ Details
:: ::
Usage: Usage:
bandcamp-dl.py <url> bandcamp-dl [url]
bandcamp-dl.py [--template=<template>] [--base-dir=<dir>] bandcamp-dl [--template=<template>] [--base-dir=<dir>]
[--full-album] [--full-album]
(<url> | --artist=<artist> --album=<album>) (<url> | --artist=<artist> --album=<album>)
[--overwrite] [--overwrite]
[--no-art] [--no-art]
bandcamp-dl.py (-h | --help) [--embed-lyrics]
bandcamp-dl.py (--version) [--group]
[--embed-art]
[--debug]
bandcamp-dl (-h | --help)
bandcamp-dl (--version)
Options Options
======= =======
@ -61,15 +65,20 @@ Options
:: ::
Options: Options:
-h --help Show this screen. -h --help Show this screen.
-v --version Show version. -v --version Show version.
--artist=<artist> The artist's slug (from the URL) -a --artist=<artist> The artist's slug (from the URL)
--album=<album> The album's slug (from the URL) -b --album=<album> The album's slug (from the URL)
--template=<template> Output filename template. -t --template=<template> Output filename template.
[default: %{artist}/%{album}/%{track} - %{title}] [default: %{artist}/%{album}/%{track} - %{title}]
--base-dir=<dir> Base location of which all files are downloaded -d --base-dir=<dir> Base location of which all files are downloaded.
-o --overwrite Overwrite tracks that already exist. Default is False. -f --full-album Download only if all tracks are available.
-n --no-art Skip grabbing album art -o --overwrite Overwrite tracks that already exist. Default is False.
-n --no-art Skip grabbing album art
-e --embed-lyrics Embed track lyrics (If available)
-g --group Use album/track Label as iTunes grouping
-r --embed-art Embed album art (If available)
-u --debug Log debug information to a file
Filename Template Filename Template
================= =================
@ -90,10 +99,7 @@ Bugs
==== ====
Bugs should be reported `here <https://github.com/iheanyi/bandcamp-dl/issues>`_. Bugs should be reported `here <https://github.com/iheanyi/bandcamp-dl/issues>`_.
Please include the full output of the command when run with ``--verbose``. Please include the URL and/or options used.
The output (including the first lines) contain important debugging information.
Issues without the full output are often not reproducible and therefore
do not get solved in short order, if ever.
For discussions, join us in `Discord <https://discord.gg/nwdT4MP>`_. For discussions, join us in `Discord <https://discord.gg/nwdT4MP>`_.
@ -128,24 +134,6 @@ Many feature requests are for features that actually exist already!
Please, absolutely do show off your work in the issue report and detail Please, absolutely do show off your work in the issue report and detail
how the existing similar options do *not* solve your problem. how the existing similar options do *not* solve your problem.
Is there enough context in your bug report?
===========================================
People want to solve problems, and often think they do us a favor by
breaking down their larger problems (e.g. wanting to skip already
downloaded files) to a specific request (e.g. requesting us to look
whether the file exists before downloading the info page). However, what
often happens is that they break down the problem into two steps: One
simple, and one impossible (or extremely complicated one).
We are then presented with a very complicated request when the original
problem could be solved far easier, e.g. by recording the downloaded
video IDs in a separate file. To avoid this, you must include the
greater context where it is non-obvious. In particular, every feature
request that does not consist of adding support for a new site should
contain a use case scenario that explains in what situation the missing
feature would be useful.
Does the issue involve one problem, and one problem only? Does the issue involve one problem, and one problem only?
========================================================= =========================================================
@ -157,20 +145,10 @@ mark the issue as closed. Typically, reporting a bunch of issues leads
to the ticket lingering since nobody wants to attack that behemoth, to the ticket lingering since nobody wants to attack that behemoth,
until someone mercifully splits the issue into multiple ones. until someone mercifully splits the issue into multiple ones.
In particular, every site support request issue should only pertain to
services at one site (generally under a common domain, but always using
the same backend technology). Do not request support for vimeo user
videos, Whitehouse podcasts, and Google Plus pages in the same issue.
Also, make sure that you don't post bug reports alongside feature
requests. As a rule of thumb, a feature request does not include outputs
of bandcamp-dl that are not immediately related to the feature at hand.
Do not post reports of a network error alongside the request for a new
video service.
Is anyone going to need the feature? Is anyone going to need the feature?
==================================== ====================================
Only post features that you (or an incapacitated friend you can Only post features that you (or an incapable friend you can
personally talk to) require. Do not post features because they seem like personally talk to) require. Do not post features because they seem like
a good idea. If they are really useful, they will be requested by a good idea. If they are really useful, they will be requested by
someone who requires them. someone who requires them.

View File

@ -4,11 +4,12 @@ Usage:
bandcamp-dl [url] bandcamp-dl [url]
bandcamp-dl [--template=<template>] [--base-dir=<dir>] bandcamp-dl [--template=<template>] [--base-dir=<dir>]
[--full-album] [--full-album]
(<url> | --artist=<artist> --album=<album>) (<url> | --artist=<artist> --album=<album> | --artist=<artist> --track=<track>)
[--overwrite] [--overwrite]
[--no-art] [--no-art]
[--embed-lyrics] [--embed-lyrics]
[--group] [--group]
[--embed-art]
bandcamp-dl (-h | --help) bandcamp-dl (-h | --help)
bandcamp-dl (--version) bandcamp-dl (--version)
@ -16,6 +17,7 @@ Options:
-h --help Show this screen. -h --help Show this screen.
-v --version Show version. -v --version Show version.
-a --artist=<artist> The artist's slug (from the URL) -a --artist=<artist> The artist's slug (from the URL)
-s --track=<track> The track's slug (from the URL)
-b --album=<album> The album's slug (from the URL) -b --album=<album> The album's slug (from the URL)
-t --template=<template> Output filename template. -t --template=<template> Output filename template.
[default: %{artist}/%{album}/%{track} - %{title}] [default: %{artist}/%{album}/%{track} - %{title}]
@ -25,6 +27,7 @@ Options:
-n --no-art Skip grabbing album art -n --no-art Skip grabbing album art
-e --embed-lyrics Embed track lyrics (If available) -e --embed-lyrics Embed track lyrics (If available)
-g --group Use album/track Label as iTunes grouping -g --group Use album/track Label as iTunes grouping
-r --embed-art Embed album art (If available)
""" """
""" """
Coded by: Coded by:
@ -55,25 +58,9 @@ from docopt import docopt
from bandcamp_dl.bandcamp import Bandcamp from bandcamp_dl.bandcamp import Bandcamp
from bandcamp_dl.bandcampdownloader import BandcampDownloader from bandcamp_dl.bandcampdownloader import BandcampDownloader
# LOGGING #######################
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler('bandcamp-dl_0.0.7-09-DEBUG.log')
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
# LOGGING #######################
def main(): def main():
arguments = docopt(__doc__, version='bandcamp-dl 0.0.7-09') arguments = docopt(__doc__, version='bandcamp-dl 0.0.8')
logger.debug('\n\tArguments: {}\n'.format(arguments))
bandcamp = Bandcamp() bandcamp = Bandcamp()
@ -83,14 +70,16 @@ def main():
if os.path.isfile(session_file): if os.path.isfile(session_file):
with open(session_file, "r") as f: with open(session_file, "r") as f:
arguments = ast.literal_eval(f.readline()) arguments = ast.literal_eval(f.readline())
elif arguments['<url>'] is None: elif arguments['<url>'] is None and arguments['--artist'] is None:
print(__doc__) print(__doc__)
else: else:
with open(session_file, "w") as f: with open(session_file, "w") as f:
f.write("".join(str(arguments).split('\n'))) f.write("".join(str(arguments).split('\n')))
if arguments['--artist'] and arguments['--album']: if arguments['--artist'] and arguments['--album']:
url = Bandcamp.generate_album_url(arguments['--artist'], arguments['--album']) url = Bandcamp.generate_album_url(arguments['--artist'], arguments['--album'], "album")
elif arguments['--artist'] and arguments['--track']:
url = Bandcamp.generate_album_url(arguments['--artist'], arguments['--track'], "track")
else: else:
url = arguments['<url>'] url = arguments['<url>']
@ -105,8 +94,10 @@ def main():
print("Full album not available. Skipping...") print("Full album not available. Skipping...")
else: else:
bandcamp_downloader = BandcampDownloader(url, arguments['--template'], basedir, arguments['--overwrite'], bandcamp_downloader = BandcampDownloader(url, arguments['--template'], basedir, arguments['--overwrite'],
arguments['--embed-lyrics'], arguments['--group']) arguments['--embed-lyrics'], arguments['--group'],
arguments['--embed-art'], arguments['--debug'])
bandcamp_downloader.start(album) bandcamp_downloader.start(album)
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -7,22 +7,11 @@ from bs4 import FeatureNotFound
from bandcamp_dl.bandcampjson import BandcampJSON from bandcamp_dl.bandcampjson import BandcampJSON
# LOGGING #######################
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler('bandcamp-dl_0.0.7-09-DEBUG.log')
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
# LOGGING #######################
class Bandcamp: class Bandcamp:
def __init__(self, debug=False):
self.debug = debug
def parse(self, url: str, art: bool=True) -> dict or None: def parse(self, url: str, art: bool=True) -> dict or None:
"""Requests the page, cherry picks album info """Requests the page, cherry picks album info
@ -40,8 +29,6 @@ class Bandcamp:
except FeatureNotFound: except FeatureNotFound:
self.soup = BeautifulSoup(response.text, "html.parser") self.soup = BeautifulSoup(response.text, "html.parser")
logger.debug('\n\tBeautifulSoup: {}\n'.format(self.soup))
bandcamp_json = BandcampJSON(self.soup).generate() bandcamp_json = BandcampJSON(self.soup).generate()
album_json = json.loads(bandcamp_json[0]) album_json = json.loads(bandcamp_json[0])
embed_json = json.loads(bandcamp_json[1]) embed_json = json.loads(bandcamp_json[1])
@ -121,14 +108,15 @@ class Bandcamp:
return track_metadata return track_metadata
@staticmethod @staticmethod
def generate_album_url(artist: str, album: str) -> str: def generate_album_url(artist: str, slug: str, page_type: str) -> str:
"""Generate an album url based on the artist and album name """Generate an album url based on the artist and album name
:param artist: artist name :param artist: artist name
:param album: album name :param slug: Slug of album/track
:return: album url as str :param page_type: Type of page album/track
:return: url as str
""" """
return "http://{0}.bandcamp.com/album/{1}".format(artist, album) return "http://{0}.bandcamp.com/{1}/{2}".format(artist, 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

View File

@ -6,6 +6,7 @@ from mutagen.mp3 import MP3, EasyMP3
from mutagen.id3._frames import TIT1 from mutagen.id3._frames import TIT1
from mutagen.id3._frames import TIT2 from mutagen.id3._frames import TIT2
from mutagen.id3._frames import USLT from mutagen.id3._frames import USLT
from mutagen.id3._frames import APIC
from slugify import slugify from slugify import slugify
if not sys.version_info[:2] == (3, 6): if not sys.version_info[:2] == (3, 6):
@ -14,7 +15,8 @@ if not sys.version_info[:2] == (3, 6):
class BandcampDownloader: class BandcampDownloader:
def __init__(self, urls=None, template=None, directory=None, overwrite=False, lyrics=None, grouping=None): def __init__(self, urls=None, template=None, directory=None, overwrite=False, lyrics=None, grouping=None,
embed_art=None, debug=False):
"""Initialize variables we will need throughout the Class """Initialize variables we will need throughout the Class
:param urls: list of urls :param urls: list of urls
@ -22,7 +24,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/0.0.7-09 (https://github.com/iheanyi/bandcamp-dl)'} self.headers = {'user_agent': 'bandcamp-dl/0.0.8 (https://github.com/iheanyi/bandcamp-dl)'}
self.session = requests.Session() self.session = requests.Session()
if type(urls) is str: if type(urls) is str:
@ -34,6 +36,8 @@ class BandcampDownloader:
self.overwrite = overwrite self.overwrite = overwrite
self.lyrics = lyrics self.lyrics = lyrics
self.grouping = grouping self.grouping = grouping
self.embed_art = embed_art
self.debug = debug
def start(self, album: dict): def start(self, album: dict):
"""Start album download process """Start album download process
@ -60,10 +64,12 @@ class BandcampDownloader:
path = self.template path = self.template
path = path.replace("%{artist}", slugify(track['artist'])) path = path.replace("%{artist}", slugify(track['artist']))
path = path.replace("%{album}", slugify(track['album'])) path = path.replace("%{album}", slugify(track['album']))
if track['track'] == "None": if track['track'] == "None":
path = path.replace("%{track}", "Single") path = path.replace("%{track}", "Single")
else: else:
path = path.replace("%{track}", str(track['track']).zfill(2)) path = path.replace("%{track}", str(track['track']).zfill(2))
path = path.replace("%{title}", slugify(track['title'])) path = path.replace("%{title}", slugify(track['title']))
path = u"{0}/{1}.{2}".format(self.directory, path, "mp3") path = u"{0}/{1}.{2}".format(self.directory, path, "mp3")
@ -108,6 +114,16 @@ class BandcampDownloader:
filename = filepath.rsplit('/', 1)[1] filename = filepath.rsplit('/', 1)[1]
dirname = self.create_directory(filepath) dirname = self.create_directory(filepath)
if album['art'] and not os.path.exists(dirname + "/cover.jpg"):
try:
with open(dirname + "/cover.jpg", "wb") as f:
r = self.session.get(album['art'])
f.write(r.content)
self.album_art = dirname + "/cover.jpg"
except Exception as e:
print(e)
print("Couldn't download album art.")
attempts = 0 attempts = 0
skip = False skip = False
@ -165,17 +181,14 @@ class BandcampDownloader:
return False return False
if skip is not True: if skip is not True:
self.write_id3_tags(filepath, track_meta) self.write_id3_tags(filepath, track_meta)
if album['art']:
try:
with open(dirname + "/cover.jpg", "wb") as f:
r = self.session.get(album['art'])
f.write(r.content)
except Exception as e:
print(e)
print("Couldn't download album art.")
if os.path.isfile("not.finished"): if os.path.isfile("not.finished"):
os.remove("not.finished") os.remove("not.finished")
# Remove album art image as it is embedded
if self.embed_art:
os.remove(self.album_art)
return True return True
def write_id3_tags(self, filepath: str, meta: dict): def write_id3_tags(self, filepath: str, meta: dict):
@ -199,6 +212,10 @@ class BandcampDownloader:
audio["TIT1"] = TIT1(encoding=3, text=meta["label"]) audio["TIT1"] = TIT1(encoding=3, text=meta["label"])
if self.lyrics: if self.lyrics:
audio["USLT"] = USLT(encoding=3, lang='eng', desc='', text=meta['lyrics']) audio["USLT"] = USLT(encoding=3, lang='eng', desc='', text=meta['lyrics'])
if self.embed_art:
with open(self.album_art, 'rb') as cover_img:
cover_bytes = cover_img.read()
audio["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=cover_bytes)
audio.save() audio.save()
audio = EasyMP3(filepath) audio = EasyMP3(filepath)

View File

@ -2,20 +2,6 @@ import re
import demjson import demjson
# LOGGING #######################
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler('bandcamp-dl_0.0.7-09-DEBUG.log')
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
# LOGGING #######################
class BandcampJSON: class BandcampJSON:
def __init__(self, body): def __init__(self, body):
@ -32,7 +18,6 @@ class BandcampJSON:
self.regex = re.compile(r"(?<=var\s" + target + "\s=\s).*?(?=};)", re.DOTALL) self.regex = re.compile(r"(?<=var\s" + target + "\s=\s).*?(?=};)", re.DOTALL)
self.target = target self.target = target
self.js_to_json() self.js_to_json()
logger.debug('\n\tPreliminary JSON data: {}\n'.format(self.json_data))
return self.json_data return self.json_data
def get_pagedata(self): def get_pagedata(self):

View File

@ -10,7 +10,7 @@ here = path.abspath(path.dirname(__file__))
setup( setup(
name='bandcamp-downloader', name='bandcamp-downloader',
version='0.0.7-09', version='0.0.8',
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(),
url='https://github.com/iheanyi/bandcamp-dl', url='https://github.com/iheanyi/bandcamp-dl',