sane-tag-music: add a method which generalizes tags to one file from the rest of the album (e.g. the album name)
This commit is contained in:
@@ -90,6 +90,7 @@ options:
|
||||
|
||||
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
|
||||
@@ -256,36 +257,42 @@ class Tags:
|
||||
self.title = title or []
|
||||
self.tracknumber = tracknumber or []
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"artist:{self.producer}/{self.albumartist}/{self.artist}, album:{self.album}, title:{self.title}, trackno:{self.tracknumber}"
|
||||
|
||||
def union(self, fallback: 'Tags') -> 'Tags':
|
||||
def merge(self, other: 'Tags', merge_field) -> 'Tags':
|
||||
return Tags(
|
||||
album=merge_field(self.album, other.album),
|
||||
albumartist=merge_field(self.albumartist, other.albumartist),
|
||||
artist=merge_field(self.artist, other.artist),
|
||||
producer=merge_field(self.producer, other.producer),
|
||||
title=merge_field(self.title, other.title),
|
||||
tracknumber=merge_field(self.tracknumber, other.tracknumber),
|
||||
)
|
||||
|
||||
def or_(self, other: 'Tags') -> 'Tags':
|
||||
"""
|
||||
substitute any tags missing tags in `self` with those from `fallback`.
|
||||
i.e. `self` takes precedence over `fallback`.
|
||||
"""
|
||||
def merge_field(primary: list[str], secondary: list[str]) -> list[str]:
|
||||
# primary_lower = [i.lower() for i in primary]
|
||||
# return primary + [i for i in secondary if i.lower() not in primary_lower]
|
||||
return primary or secondary
|
||||
# def merge_field(primary: list[str], secondary: list[str]) -> list[str]:
|
||||
# # primary_lower = [i.lower() for i in primary]
|
||||
# # return primary + [i for i in secondary if i.lower() not in primary_lower]
|
||||
# return primary or secondary
|
||||
return self.merge(other, lambda a, b: a or b)
|
||||
|
||||
def union(self, other: 'Tags') -> 'Tags':
|
||||
def merge_field(a: list[str], b: list[str]) -> list[str]:
|
||||
return a + [i for i in b if i not in a]
|
||||
return self.merge(other, merge_field)
|
||||
|
||||
album=merge_field(self.album, fallback.album)
|
||||
albumartist=merge_field(self.albumartist, fallback.albumartist)
|
||||
artist=merge_field(self.artist, fallback.artist)
|
||||
producer=merge_field(self.producer, fallback.producer)
|
||||
title=merge_field(self.title, fallback.title)
|
||||
tracknumber=merge_field(self.tracknumber, fallback.tracknumber)
|
||||
|
||||
return Tags(
|
||||
album=album,
|
||||
albumartist=albumartist,
|
||||
artist=artist,
|
||||
producer=producer,
|
||||
title=title,
|
||||
tracknumber=tracknumber,
|
||||
)
|
||||
# def intersection(self, other: 'Tags') -> 'Tags':
|
||||
# def merge_field(primary: list[str], secondary: list[str]):
|
||||
# if primary == secondary:
|
||||
# return primary
|
||||
# else:
|
||||
# return []
|
||||
# return self.merge(other, merge_field)
|
||||
|
||||
def trim_fields(self) -> None:
|
||||
def trim(field: list[str]):
|
||||
@@ -736,11 +743,11 @@ class Tagger:
|
||||
path_tags = Tags.from_path(file_.path_)
|
||||
if self.force:
|
||||
# manual_tags > path_tags > old_tags
|
||||
new_tags = self.manual_tags.union(path_tags).union(old_tags)
|
||||
new_tags = self.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.union(old_tags).union(path_tags)
|
||||
new_tags = self.manual_tags.or_(old_tags).or_(path_tags)
|
||||
# special case that explicitly supplying empty tags should delete the existing
|
||||
if self.manual_tags.album == [""]:
|
||||
new_tags.album = []
|
||||
@@ -771,7 +778,7 @@ class Tagger:
|
||||
file_.write_tags(new_tags)
|
||||
|
||||
def fix_path(self, file_: MediaFile) -> None:
|
||||
tags = self.manual_tags.union(file_.tags_on_disk())
|
||||
tags = self.manual_tags.or_(file_.tags_on_disk())
|
||||
new_path = tags.to_path(os.path.splitext(file_.path_)[1])
|
||||
if new_path is None:
|
||||
logger.debug(f"skipping untagged file: {file_.path_}")
|
||||
@@ -828,6 +835,23 @@ def walk_files(paths: list[str], media_type: MediaType):
|
||||
continue
|
||||
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.
|
||||
"""
|
||||
all_tags = Tags()
|
||||
for f in files:
|
||||
all_tags = all_tags.union(f.tags_on_disk())
|
||||
|
||||
return Tags(
|
||||
album=all_tags.album if len(all_tags.album) == 1 else [],
|
||||
albumartist=all_tags.albumartist if len(all_tags.albumartist) == 1 else [],
|
||||
artist=all_tags.artist if len(all_tags.artist) == 1 else [],
|
||||
producer=all_tags.producer if len(all_tags.producer) == 1 else [],
|
||||
title=all_tags.title if len(all_tags.title) == 1 else [],
|
||||
tracknumber=all_tags.tracknumber if len(all_tags.tracknumber) == 1 else [],
|
||||
)
|
||||
|
||||
def main():
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
@@ -856,6 +880,7 @@ 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")
|
||||
@@ -867,6 +892,8 @@ def main():
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
files = walk_files(args.path, args.type)
|
||||
|
||||
manual_tags = Tags(
|
||||
album=[args.album] if args.album else [],
|
||||
albumartist=[args.album_artist] if args.album_artist else [],
|
||||
@@ -876,14 +903,23 @@ def main():
|
||||
tracknumber=[args.trackno] if args.trackno is not None else [],
|
||||
)
|
||||
|
||||
if getattr(args, "from_neighbors", False):
|
||||
files = list(files)
|
||||
common_tags = extract_common_tags(files)
|
||||
# clear things which are exceedingly unlikely to generalize:
|
||||
common_tags.title = []
|
||||
common_tags.tracknumber = []
|
||||
if common_tags.albumartist or manual_tags.albumartist:
|
||||
# prefer albumartist over (track) artist
|
||||
common_tags.artist = []
|
||||
manual_tags = manual_tags.or_(common_tags)
|
||||
|
||||
tagger = Tagger(
|
||||
dry_run=args.dry_run,
|
||||
force=getattr(args, "force", False),
|
||||
manual_tags=manual_tags,
|
||||
)
|
||||
|
||||
files = walk_files(args.path, args.type)
|
||||
|
||||
if args.subcommand == "show":
|
||||
for f in files:
|
||||
tagger.show(f)
|
||||
|
Reference in New Issue
Block a user