3 # Copyright © 2018-2019 Nick Bowler
5 # Tool to fetch album art for a release from the Cover Art Archive.
7 # License WTFPL2: Do What The Fuck You Want To Public License, version 2.
8 # This is free software: you are free to do what the fuck you want to.
9 # There is NO WARRANTY, to the extent permitted by law.
11 from pathlib import Path
23 progname = "caa-fetcher"
25 musicbrainzngs.set_useragent(progname, version)
27 parser = argparse.ArgumentParser(
28 description='Download album artwork from the Cover Art Archive'
31 def errmsg(msg, prog=parser.prog):
32 print("%s: %s" % (prog, msg), file=sys.stderr)
34 # NamedTemporaryFile workalike which allows control of the file mode...
35 def open_tmp(prefix="tmp", suffix="", dir=".", mode=0o600):
36 names = tempfile._get_candidate_names()
37 for seq in range(100):
38 name = os.path.join(dir, "%s%s%s" % (prefix, next(names), suffix))
40 f = open(name, "x+", mode)
41 except FileExistsError:
43 return tempfile._TemporaryFileWrapper(f, name)
46 # Given an arbitrary string, return the first substring that looks like an
47 # mbid, or None if no such substring is found.
48 def extract_mbid(arg):
49 xdigit = r'[0-9abcdefABCDEF]'
50 m = re.search(r'{0}{{8}}(-{0}{{4}}){{3}}-{0}{{12}}'.format(xdigit), arg)
54 class VersionAction(argparse.Action):
55 def __init__(self, **kw):
56 super().__init__(nargs=0, help="show a version message and exit", **kw)
57 def __call__(self, parser, namespace, values, option_string=None):
58 print("%s %s" % (progname, version))
59 print('''Copyright © 2019 Nick Bowler
60 License WTFPL2: Do What The Fuck You Want To Public License, version 2.
61 This is free software: you are free to do what the fuck you want to.
62 There is NO WARRANTY, to the extent permitted by law.''')
64 parser.add_argument('--version', action=VersionAction)
66 parser.add_argument('-o', '--output-directory', metavar='DIR',
67 help='''downloaded files are written to DIR, which is
68 created if it does not exists. Default: .''')
69 parser.add_argument('-r', '--release-mbid', metavar='MBID', required=True)
70 args = parser.parse_args()
72 release_mbid = extract_mbid(args.release_mbid)
73 if release_mbid is None:
74 parser.error("invalid release MBID: %s" % (args.release_mbid))
76 # TODO: allow the naming scheme to be configured...
77 name_format = "{num:02d}"
80 covers = musicbrainzngs.get_image_list(release_mbid)
81 except musicbrainzngs.ResponseError as e:
82 errmsg("error: cannot retrieve image list for release")
84 if isinstance(e.cause, urllib.error.HTTPError) and e.cause.code == 404:
85 errmsg("is %s a release MBID?" % (release_mbid))
88 if len(covers["images"]) == 0:
89 print("release has no cover art, nothing to do")
92 if args.output_directory:
93 Path(args.output_directory).mkdir(parents=True, exist_ok=True)
94 os.chdir(args.output_directory)
97 for c in covers["images"]:
98 (_,ext) = os.path.splitext(c["image"])
99 outname = (name_format + "{0}").format(ext,
100 num = (covers["images"].index(c) + 1)
103 if Path(outname).exists():
104 print("%s already exists, skipping..." % outname)
107 outfile = open_tmp(suffix=ext, dir=".", mode=0o666)
108 rc = os.spawnlp(os.P_WAIT, 'wget', 'wget', '-O', outfile.name, c["image"])
112 os.link(outfile.name, outname)