sane-tag-media: rework the tag extrapolation to be less intrusive

This commit is contained in:
2024-08-03 07:58:43 +00:00
parent 0dba9987c5
commit ee062d61d0

View File

@@ -819,22 +819,18 @@ class MediaFile:
self.meta.set_tag(self.tag_field_names.tracknumber, tags.tracknumber) self.meta.set_tag(self.tag_field_names.tracknumber, tags.tracknumber)
self.meta.flush() self.meta.flush()
class MediaFileWithNeighbors(MediaFile):
"""
a MediaFile, augmented with tags that are common to all of its neighbors
"""
neighbor_tags: Tags = Tags()
class TagsProvider: class TagsProvider:
def __init__(self, ignore_existing: bool, override_existing: bool, derive: bool, manual_tags: Tags, neighbor_tags: Tags|None = None): def __init__(self, ignore_existing: bool, override_existing: bool, derive: bool, manual_tags: Tags):
self.ignore_existing = ignore_existing self.ignore_existing = ignore_existing
self.override_existing = override_existing self.override_existing = override_existing
self.derive = derive self.derive = derive
self.manual_tags = manual_tags self.manual_tags = manual_tags
self.neighbor_tags = neighbor_tags or Tags()
def with_neighbor_tags(self, neighbor_tags: Tags) -> 'TagsProvider':
return TagsProvider(
ignore_existing=self.ignore_existing,
override_existing=self.override_existing,
derive=self.derive,
manual_tags=self.manual_tags,
neighbor_tags=neighbor_tags,
)
def can_derive_from_neighbors(self) -> bool: def can_derive_from_neighbors(self) -> bool:
return self.derive and not self.ignore_existing return self.derive and not self.ignore_existing
@@ -856,7 +852,10 @@ class TagsProvider:
# we can't generalize *any* tags to an artist item (e.g. Justice/artist.png) # we can't generalize *any* tags to an artist item (e.g. Justice/artist.png)
return my_derived_tags return my_derived_tags
neighbor_tags = self.neighbor_tags if not isinstance(file_, MediaFileWithNeighbors):
return my_derived_tags
neighbor_tags = file_.neighbor_tags
if self.override_existing: if self.override_existing:
# our derived tags overrule anything generalized from our neighbors # our derived tags overrule anything generalized from our neighbors
return my_derived_tags.or_(neighbor_tags) return my_derived_tags.or_(neighbor_tags)
@@ -963,29 +962,28 @@ class Gatherer:
self.media_type = media_type self.media_type = media_type
self.tags_provider = tags_provider self.tags_provider = tags_provider
def files(self) -> list[tuple[MediaFile, Tags]]: def files(self) -> list[MediaFile]:
""" """
returns a list where each item is a tuple of: iterates over files which match the media_type.
- a file to be processed note that the yielded file may actually be a more specialized MediaFileWithNeighbors instance,
- additional tags which are applicable to that file in case we're deriving tags from neighboring files.
""" """
for root in self.roots: for root in self.roots:
_tags_seen, files = self.files_below(root) _tags_seen, files = self.files_below(root)
for file_, tags in files: for file_ in files:
if self.media_type is not None and not file_.is_type(self.media_type): if self.media_type is not None and not file_.is_type(self.media_type):
continue continue
yield file_, tags yield file_
def files_below(self, root: str) -> tuple[Tags, list[tuple[MediaFile, Tags]]]: def files_below(self, root: str) -> tuple[Tags, list[MediaFileWithNeighbors]]:
""" """
returns: (tags_seen, files) returns: (tags_seen, files)
where each file is (file, derived_tag)
""" """
if not os.path.isdir(root): if not os.path.isdir(root):
# single file # single file
file_ = MediaFile.new(root) file_ = MediaFileWithNeighbors.new(root)
return self.tags_provider.on_disk(file_), [ (file_, Tags()) ] return self.tags_provider.on_disk(file_), [ file_ ]
if not self.tags_provider.can_derive_from_neighbors(): if not self.tags_provider.can_derive_from_neighbors():
# directory, but don't derive any tags # directory, but don't derive any tags
@@ -1010,8 +1008,9 @@ class Gatherer:
derived_tags.tracknumber = [] derived_tags.tracknumber = []
def _gen(): def _gen():
for files_below in filelists: for files_below in filelists:
for file_, tags in files_below: for file_ in files_below:
yield file_, derived_tags file_.neighbor_tags = file_.neighbor_tags.or_(derived_tags)
yield file_
return tags_seen, _gen() return tags_seen, _gen()
@@ -1075,22 +1074,20 @@ def main():
gatherer = Gatherer(args.path, args.type, tags_provider) gatherer = Gatherer(args.path, args.type, tags_provider)
def gather_files(): tagger = Tagger(dry_run=args.dry_run, tags_provider=tags_provider)
for f, tags in gatherer.files():
yield Tagger(dry_run=args.dry_run, tags_provider=tags_provider.with_neighbor_tags(tags)), f
if args.subcommand == "show": if args.subcommand == "show":
for tagger, f in gather_files(): for f in gatherer.files():
tagger.show(f) tagger.show(f)
elif args.subcommand == "show_missing": elif args.subcommand == "show_missing":
for tagger, f in gather_files(): for f in gatherer.files():
if not tagger.is_sufficiently_tagged(f): if not tagger.is_sufficiently_tagged(f):
tagger.show(f) tagger.show(f)
elif args.subcommand == "fix_tags": elif args.subcommand == "fix_tags":
for tagger, f in gather_files(): for f in gatherer.files():
tagger.tag_file(f) tagger.tag_file(f)
elif args.subcommand == "fix_paths": elif args.subcommand == "fix_paths":
for tagger, f in gather_files(): for f in gatherer.files():
tagger.fix_path(f) tagger.fix_path(f)
else: else:
assert False, f"unrecognized command {args.subcommand}" assert False, f"unrecognized command {args.subcommand}"