flacinfo.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/env python3
  2. """
  3. flacinfo
  4. A script analoguous to `mp3info`, allowing one to easily tag their music
  5. collection, but for flac files.
  6. """
  7. import argparse
  8. import subprocess
  9. import os
  10. import sys
  11. VORBIS_ARG_NAME = {
  12. "title": "TITLE",
  13. "track": "TRACKNUMBER",
  14. "artist": "ARTIST",
  15. "album": "ALBUM",
  16. "albumartist": "ALBUMARTIST",
  17. "albumnumber": "DISCNUMBER",
  18. "genre": "GENRE",
  19. "year": "DATE",
  20. "comment": "COMMENT",
  21. }
  22. class NoSuchTag(Exception):
  23. """Raised when trying to reverse the `VORBIS_ARG_NAME` dict on an invalid tag name"""
  24. def __init__(self, tag):
  25. super().__init__()
  26. self.tag = tag
  27. def __str__(self):
  28. return "No such Vorbis tag {}".format(self.tag)
  29. class MetaflacError(Exception):
  30. """ Raised when an invocation of metaflac failed. """
  31. def argparser():
  32. """ Parses the arguments from sys.argv """
  33. parser = argparse.ArgumentParser(
  34. description="Edit flac files' metadata",
  35. epilog=(
  36. "When no option modifying the tags is passed, the currently "
  37. "set tags are shown."
  38. ),
  39. )
  40. parser.add_argument("-a", "--artist", help="Specify artist name")
  41. parser.add_argument("-c", "--comment", help="Specify an arbitrary comment")
  42. parser.add_argument("-g", "--genre", help="Specify genre (in plain text)")
  43. parser.add_argument("-l", "--album", help="Specify album name")
  44. parser.add_argument("-m", "--albumnumber", help="Specify album number")
  45. parser.add_argument("-n", "--track", help="Specify track number")
  46. parser.add_argument("-t", "--title", help="Specify track title")
  47. parser.add_argument("-y", "--year", help="Specify album year")
  48. parser.add_argument("-A", "--albumartist", help="Specify album artist")
  49. parser.add_argument(
  50. "file", nargs="+", metavar="FILE", help="The file(s) to work on"
  51. )
  52. return parser.parse_args()
  53. def is_flac_file(path):
  54. """ Checks whether `path` refers to an existing, writeable flac file """
  55. if not os.path.isfile(path) or not os.access(path, os.W_OK):
  56. return False
  57. try:
  58. subprocess.run(
  59. ["metaflac", "--list", path],
  60. stdout=subprocess.DEVNULL,
  61. stderr=subprocess.DEVNULL,
  62. check=True,
  63. )
  64. except subprocess.CalledProcessError:
  65. return False # Metaflac failed to list the files' metadata
  66. return True
  67. def make_metaflac_args(in_args):
  68. out_args = []
  69. for arg in in_args:
  70. arg_val = in_args[arg]
  71. if arg not in VORBIS_ARG_NAME or arg_val is None:
  72. continue
  73. arg_name = VORBIS_ARG_NAME[arg]
  74. out_args.append("--remove-tag={}".format(arg_name))
  75. out_args.append("--set-tag={}={}".format(arg_name, arg_val))
  76. return out_args
  77. def edit_flac(args):
  78. """ Perfoms the requested edition operations """
  79. metaflac_args = make_metaflac_args(args)
  80. metaflac_args += args["file"]
  81. metaflac_args.insert(0, "metaflac")
  82. try:
  83. subprocess.run(metaflac_args, check=True)
  84. except subprocess.CalledProcessError as exn:
  85. raise MetaflacError(
  86. "Failed to edit tags: metaflac exited with error {}. Output:\n{}".format(
  87. exn.returncode, exn.stderr
  88. )
  89. ) from exn
  90. def reverse_tag(vorbis_tag):
  91. """ Reverses a Vorbis tag to an argument name """
  92. for tag in VORBIS_ARG_NAME:
  93. if VORBIS_ARG_NAME[tag] == vorbis_tag:
  94. return tag
  95. raise NoSuchTag(vorbis_tag)
  96. def get_tags(path):
  97. """ Retrieves the relevant tags for a single file """
  98. metaflac_args = ["metaflac"]
  99. for tag in VORBIS_ARG_NAME:
  100. metaflac_args += ["--show-tag", VORBIS_ARG_NAME[tag]]
  101. metaflac_args.append(path)
  102. try:
  103. metaflac_run = subprocess.run(metaflac_args, check=True, stdout=subprocess.PIPE)
  104. except subprocess.CalledProcessError as exn:
  105. raise MetaflacError(
  106. (
  107. "Failed to get tags for {}: metaflac exited with error {}."
  108. "Output:\n{}"
  109. ).format(path, exn.returncode, exn.stderr)
  110. ) from exn
  111. meta_out = metaflac_run.stdout.decode("utf-8")
  112. output = {}
  113. for line in meta_out.split("\n"):
  114. split = line.split("=")
  115. tag, value = split[0], "=".join(split[1:])
  116. if not tag:
  117. continue
  118. tag = reverse_tag(tag)
  119. output[tag] = value
  120. return output
  121. def show_tags(path):
  122. """ Shows the relevant tags already present in the given flac file """
  123. tags = get_tags(path)
  124. print("File: {}".format(path))
  125. tag_len = max([len(tag) for tag in tags]) + 1
  126. for tag in tags:
  127. print((" {:<" + str(tag_len) + "} {}").format(tag + ":", tags[tag]))
  128. print("")
  129. def main():
  130. """ Entrypoint function """
  131. args = vars(argparser())
  132. has_errors = False
  133. for cur_file in args["file"]:
  134. if not is_flac_file(cur_file):
  135. print(
  136. (
  137. "Error: file {} does not exist, or is not writeable by " "metaflac"
  138. ).format(cur_file),
  139. file=sys.stderr,
  140. )
  141. has_errors = True
  142. if has_errors:
  143. print("One or more file cannot be manipulated. Aborting.", file=sys.stderr)
  144. sys.exit(1)
  145. edit_mode = False
  146. for tag in VORBIS_ARG_NAME:
  147. if args[tag] is not None:
  148. edit_mode = True
  149. break
  150. if edit_mode:
  151. edit_flac(args)
  152. else:
  153. for path in args["file"]:
  154. show_tags(path)
  155. if __name__ == "__main__":
  156. main()