sane-tag-music: allow deriving tags for all operations, not just fix-tags
This commit is contained in:
@@ -86,11 +86,11 @@ options:
|
||||
--producer PRODUCER use when the artist is a pseudonym, and this is their umbrella name.
|
||||
--title TITLE
|
||||
--trackno TRACK_NUMBER
|
||||
--derive apply existing tags (e.g. album) found in the file set to any files in the set missing such tags
|
||||
--type audio|image skip files which aren't of some specific media type
|
||||
|
||||
fix-tags options:
|
||||
--force: apply path-based tag to each file, even those which already have tags
|
||||
--from-neighbors: apply existing tags (e.g. album) found in the file set to any files in the set missing such tags
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -537,6 +537,20 @@ class MetadataImpl:
|
||||
def flush(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
class InMemoryMetadata(MetadataImpl):
|
||||
def __init__(self, path_: str):
|
||||
super().__init__(path_)
|
||||
self.tags = {}
|
||||
|
||||
def get_tag(self, name: str) -> list[str]:
|
||||
return self.tags.get(name, [])
|
||||
|
||||
def set_tag(self, name: str, value: list[str]) -> None:
|
||||
self.tags[name] = value
|
||||
|
||||
def flush(self) -> None:
|
||||
logger.debug(f"not writing tags for unsupported file {self.path_}")
|
||||
|
||||
class MutagenMetadata(MetadataImpl):
|
||||
def __init__(self, path_: str, muta):
|
||||
super().__init__(path_=path_)
|
||||
@@ -647,7 +661,7 @@ class MediaFile:
|
||||
return os.path.splitext(self.path_)[1][1:].lower()
|
||||
|
||||
@staticmethod
|
||||
def new(f: str) -> "MediaFile | None":
|
||||
def new(f: str) -> 'MediaFile':
|
||||
ext = os.path.splitext(f)[1][1:].lower()
|
||||
tag_field_names = TagFieldNames()
|
||||
meta = None
|
||||
@@ -686,7 +700,7 @@ class MediaFile:
|
||||
logger.debug(f"no metadata handler for {f}")
|
||||
|
||||
if meta is None:
|
||||
return None
|
||||
meta = InMemoryMetadata(f)
|
||||
|
||||
return MediaFile(f, meta, tag_field_names)
|
||||
|
||||
@@ -718,18 +732,19 @@ class MediaFile:
|
||||
self.meta.flush()
|
||||
|
||||
class Tagger:
|
||||
def __init__(self, dry_run: bool, force: bool, manual_tags: Tags):
|
||||
def __init__(self, dry_run: bool, force: bool, derive_from_path: bool, manual_tags: Tags):
|
||||
self.dry_run = dry_run
|
||||
self.force = force
|
||||
self.manual_tags = manual_tags
|
||||
self.derive_from_path = derive_from_path
|
||||
|
||||
def show(self, file_: MediaFile) -> None:
|
||||
tags = file_.tags_on_disk()
|
||||
tags = self.tags_for(file_)
|
||||
logger.info(f"tags for {file_.path_}:")
|
||||
logger.info(f" {tags}")
|
||||
|
||||
def is_sufficiently_tagged(self, file_: MediaFile) -> bool:
|
||||
tags = file_.tags_on_disk()
|
||||
tags = self.tags_for(file_)
|
||||
# N.B.: track number isn't wholly necessary; just a nice-to-have.
|
||||
if (tags.artist or tags.albumartist) and tags.album and tags.title:
|
||||
return True
|
||||
@@ -738,28 +753,32 @@ class Tagger:
|
||||
return True
|
||||
return False
|
||||
|
||||
def tag_file(self, file_: MediaFile) -> None:
|
||||
def tags_for(self, file_: MediaFile) -> Tags:
|
||||
"""
|
||||
return the tags stored in @file_, plus any we can derive from its path or our manual tags.
|
||||
"""
|
||||
old_tags = file_.tags_on_disk()
|
||||
path_tags = Tags.from_path(file_.path_)
|
||||
path_tags = Tags.from_path(file_.path_) if self.derive_from_path else Tags()
|
||||
manual_tags = self.manual_tags
|
||||
if self.force:
|
||||
# manual_tags > path_tags > old_tags
|
||||
new_tags = self.manual_tags.or_(path_tags).or_(old_tags)
|
||||
new_tags = manual_tags.or_(path_tags).or_(old_tags)
|
||||
else:
|
||||
# manual_tags > old_tags > path_tags
|
||||
# old_tags overrule path_tags
|
||||
new_tags = self.manual_tags.or_(old_tags).or_(path_tags)
|
||||
new_tags = manual_tags.or_(old_tags).or_(path_tags)
|
||||
# special case that explicitly supplying empty tags should delete the existing
|
||||
if self.manual_tags.album == [""]:
|
||||
if manual_tags.album == [""]:
|
||||
new_tags.album = []
|
||||
if self.manual_tags.albumartist == [""]:
|
||||
if manual_tags.albumartist == [""]:
|
||||
new_tags.albumartist = []
|
||||
if self.manual_tags.artist == [""]:
|
||||
if manual_tags.artist == [""]:
|
||||
new_tags.artist = []
|
||||
if self.manual_tags.producer == [""]:
|
||||
if manual_tags.producer == [""]:
|
||||
new_tags.producer = []
|
||||
if self.manual_tags.title == [""]:
|
||||
if manual_tags.title == [""]:
|
||||
new_tags.title = []
|
||||
if self.manual_tags.tracknumber == [""]:
|
||||
if manual_tags.tracknumber == [""]:
|
||||
new_tags.tracknumber = []
|
||||
new_tags.trim_fields()
|
||||
new_tags.cleanup_trackno()
|
||||
@@ -768,6 +787,11 @@ class Tagger:
|
||||
new_tags.demote_producer()
|
||||
new_tags.rewrite_singles()
|
||||
|
||||
return new_tags
|
||||
|
||||
def tag_file(self, file_: MediaFile) -> None:
|
||||
old_tags = file_.tags_on_disk()
|
||||
new_tags = self.tags_for(file_)
|
||||
if new_tags == old_tags:
|
||||
return self.skip_unchanged(file_.path_, old_tags)
|
||||
|
||||
@@ -778,7 +802,7 @@ class Tagger:
|
||||
file_.write_tags(new_tags)
|
||||
|
||||
def fix_path(self, file_: MediaFile) -> None:
|
||||
tags = self.manual_tags.or_(file_.tags_on_disk())
|
||||
tags = self.tags_for(file_)
|
||||
new_path = tags.to_path(os.path.splitext(file_.path_)[1])
|
||||
if new_path is None:
|
||||
logger.debug(f"skipping untagged file: {file_.path_}")
|
||||
@@ -825,16 +849,16 @@ def walk_paths(*roots: str) -> None:
|
||||
else:
|
||||
yield root
|
||||
|
||||
def walk_files(paths: list[str], media_type: MediaType):
|
||||
def walk_files(paths: list[str]):
|
||||
for path_ in walk_paths(*paths):
|
||||
file_ = MediaFile.new(path_)
|
||||
if not file_:
|
||||
logger.debug(f"skipping unsupported file: {path_}")
|
||||
continue
|
||||
if media_type is not None and not file_.is_type(media_type):
|
||||
continue
|
||||
yield file_
|
||||
|
||||
def filter_files(files: list[MediaFile], media_type: MediaType|None = None):
|
||||
for file_ in files:
|
||||
if media_type is None or file_.is_type(media_type):
|
||||
yield file_
|
||||
|
||||
def extract_common_tags(files: list[MediaFile]) -> Tags:
|
||||
"""
|
||||
return a set of tags which wouldn't contradict any existing tags among these files.
|
||||
@@ -865,6 +889,7 @@ def main():
|
||||
parser.add_argument('--producer', help="manually specify the tag")
|
||||
parser.add_argument('--title', help="manually specify the tag")
|
||||
parser.add_argument('--trackno', help="manually specify the tag")
|
||||
parser.add_argument('--derive', action='store_true', help="apply tags already existing in one file (e.g. album tag) to adjacent files in the set")
|
||||
parser.add_argument('--type', type=MediaType, help="only apply operation to a specific type of media")
|
||||
|
||||
subparsers = parser.add_subparsers(help="what to do")
|
||||
@@ -880,7 +905,6 @@ def main():
|
||||
fix_tags_parser = subparsers.add_parser("fix-tags")
|
||||
fix_tags_parser.set_defaults(subcommand="fix_tags")
|
||||
fix_tags_parser.add_argument('--force', action='store_true', help="give higher credence to path-based and manual tags than any existing tags")
|
||||
fix_tags_parser.add_argument('--from-neighbors', action='store_true', help="apply tags already existing in one file (e.g. album tag) to adjacent files in the set")
|
||||
fix_tags_parser.add_argument("path", nargs="+", help="relative path to a file to tag")
|
||||
|
||||
fix_paths_parser = subparsers.add_parser("fix-paths")
|
||||
@@ -892,7 +916,7 @@ def main():
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
files = walk_files(args.path, args.type)
|
||||
files = walk_files(args.path)
|
||||
|
||||
manual_tags = Tags(
|
||||
album=[args.album] if args.album else [],
|
||||
@@ -903,7 +927,7 @@ def main():
|
||||
tracknumber=[args.trackno] if args.trackno is not None else [],
|
||||
)
|
||||
|
||||
if getattr(args, "from_neighbors", False):
|
||||
if getattr(args, "derive", False):
|
||||
files = list(files)
|
||||
common_tags = extract_common_tags(files)
|
||||
# clear things which are exceedingly unlikely to generalize:
|
||||
@@ -917,9 +941,12 @@ def main():
|
||||
tagger = Tagger(
|
||||
dry_run=args.dry_run,
|
||||
force=getattr(args, "force", False),
|
||||
derive_from_path=getattr(args, "derive", False),
|
||||
manual_tags=manual_tags,
|
||||
)
|
||||
|
||||
files = filter_files(files, args.type)
|
||||
|
||||
if args.subcommand == "show":
|
||||
for f in files:
|
||||
tagger.show(f)
|
||||
|
Reference in New Issue
Block a user