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:
|
fix-tags options:
|
||||||
--force: apply path-based tag to each file, even those which already have tags
|
--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
|
from dataclasses import dataclass
|
||||||
@@ -256,36 +257,42 @@ class Tags:
|
|||||||
self.title = title or []
|
self.title = title or []
|
||||||
self.tracknumber = tracknumber or []
|
self.tracknumber = tracknumber or []
|
||||||
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"artist:{self.producer}/{self.albumartist}/{self.artist}, album:{self.album}, title:{self.title}, trackno:{self.tracknumber}"
|
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`.
|
substitute any tags missing tags in `self` with those from `fallback`.
|
||||||
i.e. `self` takes precedence over `fallback`.
|
i.e. `self` takes precedence over `fallback`.
|
||||||
"""
|
"""
|
||||||
def merge_field(primary: list[str], secondary: list[str]) -> list[str]:
|
# def merge_field(primary: list[str], secondary: list[str]) -> list[str]:
|
||||||
# primary_lower = [i.lower() for i in primary]
|
# # primary_lower = [i.lower() for i in primary]
|
||||||
# return primary + [i for i in secondary if i.lower() not in primary_lower]
|
# # return primary + [i for i in secondary if i.lower() not in primary_lower]
|
||||||
return primary or secondary
|
# 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)
|
# def intersection(self, other: 'Tags') -> 'Tags':
|
||||||
albumartist=merge_field(self.albumartist, fallback.albumartist)
|
# def merge_field(primary: list[str], secondary: list[str]):
|
||||||
artist=merge_field(self.artist, fallback.artist)
|
# if primary == secondary:
|
||||||
producer=merge_field(self.producer, fallback.producer)
|
# return primary
|
||||||
title=merge_field(self.title, fallback.title)
|
# else:
|
||||||
tracknumber=merge_field(self.tracknumber, fallback.tracknumber)
|
# return []
|
||||||
|
# return self.merge(other, merge_field)
|
||||||
return Tags(
|
|
||||||
album=album,
|
|
||||||
albumartist=albumartist,
|
|
||||||
artist=artist,
|
|
||||||
producer=producer,
|
|
||||||
title=title,
|
|
||||||
tracknumber=tracknumber,
|
|
||||||
)
|
|
||||||
|
|
||||||
def trim_fields(self) -> None:
|
def trim_fields(self) -> None:
|
||||||
def trim(field: list[str]):
|
def trim(field: list[str]):
|
||||||
@@ -736,11 +743,11 @@ class Tagger:
|
|||||||
path_tags = Tags.from_path(file_.path_)
|
path_tags = Tags.from_path(file_.path_)
|
||||||
if self.force:
|
if self.force:
|
||||||
# manual_tags > path_tags > old_tags
|
# 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:
|
else:
|
||||||
# manual_tags > old_tags > path_tags
|
# manual_tags > old_tags > path_tags
|
||||||
# old_tags overrule 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
|
# special case that explicitly supplying empty tags should delete the existing
|
||||||
if self.manual_tags.album == [""]:
|
if self.manual_tags.album == [""]:
|
||||||
new_tags.album = []
|
new_tags.album = []
|
||||||
@@ -771,7 +778,7 @@ class Tagger:
|
|||||||
file_.write_tags(new_tags)
|
file_.write_tags(new_tags)
|
||||||
|
|
||||||
def fix_path(self, file_: MediaFile) -> None:
|
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])
|
new_path = tags.to_path(os.path.splitext(file_.path_)[1])
|
||||||
if new_path is None:
|
if new_path is None:
|
||||||
logger.debug(f"skipping untagged file: {file_.path_}")
|
logger.debug(f"skipping untagged file: {file_.path_}")
|
||||||
@@ -828,6 +835,23 @@ def walk_files(paths: list[str], media_type: MediaType):
|
|||||||
continue
|
continue
|
||||||
yield file_
|
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():
|
def main():
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
logging.getLogger().setLevel(logging.INFO)
|
||||||
@@ -856,6 +880,7 @@ def main():
|
|||||||
fix_tags_parser = subparsers.add_parser("fix-tags")
|
fix_tags_parser = subparsers.add_parser("fix-tags")
|
||||||
fix_tags_parser.set_defaults(subcommand="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('--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_tags_parser.add_argument("path", nargs="+", help="relative path to a file to tag")
|
||||||
|
|
||||||
fix_paths_parser = subparsers.add_parser("fix-paths")
|
fix_paths_parser = subparsers.add_parser("fix-paths")
|
||||||
@@ -867,6 +892,8 @@ def main():
|
|||||||
if args.verbose:
|
if args.verbose:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
files = walk_files(args.path, args.type)
|
||||||
|
|
||||||
manual_tags = Tags(
|
manual_tags = Tags(
|
||||||
album=[args.album] if args.album else [],
|
album=[args.album] if args.album else [],
|
||||||
albumartist=[args.album_artist] if args.album_artist 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 [],
|
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(
|
tagger = Tagger(
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
force=getattr(args, "force", False),
|
force=getattr(args, "force", False),
|
||||||
manual_tags=manual_tags,
|
manual_tags=manual_tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
files = walk_files(args.path, args.type)
|
|
||||||
|
|
||||||
if args.subcommand == "show":
|
if args.subcommand == "show":
|
||||||
for f in files:
|
for f in files:
|
||||||
tagger.show(f)
|
tagger.show(f)
|
||||||
|
Reference in New Issue
Block a user