sane-tag-music: support directory/tree operations
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env nix-shell
|
#!/usr/bin/env nix-shell
|
||||||
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.mutagen ])"
|
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.mutagen ])"
|
||||||
|
#
|
||||||
|
# mutagen docs:
|
||||||
|
# - <https://mutagen.readthedocs.io/en/latest/>
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -41,16 +44,28 @@ class Tags:
|
|||||||
return f"artist:{self.artist}/{self.albumartist}, album:{self.album}, title:{self.title}, trackno:{self.tracknumber}"
|
return f"artist:{self.artist}/{self.albumartist}, album:{self.album}, title:{self.title}, trackno:{self.tracknumber}"
|
||||||
|
|
||||||
def union(self, fallback: 'Tags') -> 'Tags':
|
def union(self, fallback: 'Tags') -> 'Tags':
|
||||||
def merge_field(first: list[str], second: list[str]) -> list[str]:
|
def merge_field(primary: list[str], secondary: list[str]) -> list[str]:
|
||||||
first_lower = [i.lower() for i in first]
|
# primary_lower = [i.lower() for i in primary]
|
||||||
return first + [i for i in second if i.lower() not in first_lower]
|
# return primary + [i for i in secondary if i.lower() not in primary_lower]
|
||||||
|
return primary or secondary
|
||||||
|
|
||||||
|
|
||||||
|
artist=merge_field(self.artist, fallback.artist)
|
||||||
|
album=merge_field(self.album, fallback.album)
|
||||||
|
title=merge_field(self.title, fallback.title)
|
||||||
|
albumartist=merge_field(self.albumartist, fallback.albumartist)
|
||||||
|
tracknumber=merge_field(self.tracknumber, fallback.tracknumber)
|
||||||
|
|
||||||
|
if artist == albumartist:
|
||||||
|
# if extraneous, then keep the album artist whatever it originally was
|
||||||
|
albumartist = self.albumartist
|
||||||
|
|
||||||
return Tags(
|
return Tags(
|
||||||
artist=merge_field(self.artist, fallback.artist),
|
artist=artist,
|
||||||
album=merge_field(self.album, fallback.album),
|
album=album,
|
||||||
title=merge_field(self.title, fallback.title),
|
title=title,
|
||||||
albumartist=merge_field(self.albumartist, fallback.albumartist),
|
albumartist=albumartist,
|
||||||
tracknumber=merge_field(self.tracknumber, fallback.tracknumber),
|
tracknumber=tracknumber,
|
||||||
)
|
)
|
||||||
|
|
||||||
def promote_albumartist(self) -> None:
|
def promote_albumartist(self) -> None:
|
||||||
@@ -75,7 +90,7 @@ class Tags:
|
|||||||
comps = p.split('/')
|
comps = p.split('/')
|
||||||
tags = Tags()
|
tags = Tags()
|
||||||
if len(comps) == 3:
|
if len(comps) == 3:
|
||||||
tags.albumartist = comps[0]
|
tags.albumartist = [comps[0]]
|
||||||
album_part = comps[1].split('-')
|
album_part = comps[1].split('-')
|
||||||
if len(album_part) == 2:
|
if len(album_part) == 2:
|
||||||
# artist/artist-album/track
|
# artist/artist-album/track
|
||||||
@@ -85,7 +100,7 @@ class Tags:
|
|||||||
tags.album = [comps[1]]
|
tags.album = [comps[1]]
|
||||||
track_part = comps[2].split('-')
|
track_part = comps[2].split('-')
|
||||||
if len(track_part) == 1:
|
if len(track_part) == 1:
|
||||||
tags.title = [comps[2]]
|
tags.title = [os.path.splitext(comps[2])[0]]
|
||||||
# TODO: handle the else case
|
# TODO: handle the else case
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
@@ -104,6 +119,12 @@ class AudioFile:
|
|||||||
else:
|
else:
|
||||||
self.muta = None
|
self.muta = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def new(path_: str) -> 'AudioFile':
|
||||||
|
f = AudioFile(path_)
|
||||||
|
if f.muta is not None:
|
||||||
|
return f
|
||||||
|
|
||||||
def tags_on_disk(self) -> Tags:
|
def tags_on_disk(self) -> Tags:
|
||||||
return Tags(
|
return Tags(
|
||||||
artist=self.muta.get('artist', []) if self.muta else [],
|
artist=self.muta.get('artist', []) if self.muta else [],
|
||||||
@@ -131,7 +152,11 @@ class Tagger:
|
|||||||
self.dry_run = dry_run
|
self.dry_run = dry_run
|
||||||
|
|
||||||
def tag_file(self, path_: str) -> None:
|
def tag_file(self, path_: str) -> None:
|
||||||
file_ = AudioFile(path_)
|
file_ = AudioFile.new(path_)
|
||||||
|
if not file_:
|
||||||
|
logger.debug(f"skipping unsupported file: {path_}")
|
||||||
|
return
|
||||||
|
|
||||||
old_tags = file_.tags_on_disk()
|
old_tags = file_.tags_on_disk()
|
||||||
|
|
||||||
path_tags = Tags.from_path(path_)
|
path_tags = Tags.from_path(path_)
|
||||||
@@ -147,6 +172,17 @@ class Tagger:
|
|||||||
if self.guard_dry_run("writing tags"):
|
if self.guard_dry_run("writing tags"):
|
||||||
file_.write_tags(new_tags)
|
file_.write_tags(new_tags)
|
||||||
|
|
||||||
|
def tag_file_tree(self, root: str) -> None:
|
||||||
|
for dir_, subdirs, files_ in os.walk(root):
|
||||||
|
for f in files_:
|
||||||
|
self.tag_file(os.path.join(dir_, f))
|
||||||
|
|
||||||
|
def tag_file_or_tree(self, path_: str) -> None:
|
||||||
|
if os.path.isdir(path_):
|
||||||
|
self.tag_file_tree(path_)
|
||||||
|
else:
|
||||||
|
self.tag_file(path_)
|
||||||
|
|
||||||
def show_tagdif(self, path_: str, old_tags: Tags, new_tags: Tags):
|
def show_tagdif(self, path_: str, old_tags: Tags, new_tags: Tags):
|
||||||
logger.info(f"updating tags for {path_}")
|
logger.info(f"updating tags for {path_}")
|
||||||
logger.info(f" {old_tags}")
|
logger.info(f" {old_tags}")
|
||||||
@@ -183,7 +219,7 @@ def main():
|
|||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
tagger = Tagger(dry_run=args.dry_run)
|
tagger = Tagger(dry_run=args.dry_run)
|
||||||
tagger.tag_file(args.path)
|
tagger.tag_file_or_tree(args.path)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
Reference in New Issue
Block a user