sane-tag-media: implement --style video to aid in structuring an album-less library (e.g. an archive of Youtube videos, organized by channel)

This commit is contained in:
2024-08-18 01:51:12 +00:00
parent 7d23f9453e
commit e16a26fad2

View File

@@ -7,7 +7,7 @@ tool which runs over a complete music library (or audiobooks, videos, ebooks (li
- suggests likely tags based on file path
- from correctly-tagged files in the same directory
- or manually specified by the user
- rewrites file paths based on tags (`AlbumArtist/AlbumTitle/TrackNumber-TrackTitle`)
- rewrites file paths based on tags (by default: `AlbumArtist/AlbumTitle/TrackNumber-TrackTitle`)
- resulting paths are exclusively `[a-zA-Z0-9\._-]`
- characters outside this set are mapped to the nearest character. for example:
- `&` is replaced by `And`
@@ -34,7 +34,10 @@ options:
--override-existing apply derived tags to each file, even those which already have tags.
only makes sense when paired with --derive.
--type audio|image|text skip files which aren't of some specific media type
manually writing metadata fields:
--style music|video file/folder structure to organize library into
music (default): Album.Artist/Album/TrackNumber-Title
video: Album.Artist/Title
options for manually specifying metadata:
--album ALBUM
--album-artist ARTIST often combined with DIRECTORY to tag an entire artist or album.
--artist ARTIST track artist; usually the same as album-artist, except for compilation albums.
@@ -166,6 +169,10 @@ class MediaType(Enum):
Text = "text"
Other = "other"
class PathStyle(Enum):
Music = "music"
Video = "video"
def maybe_romanize(a: str) -> str|None:
if a == "( ͡° ͜ʖ ͡°)": return "Lenny"
if a == "かめりあ": return "Camellia"
@@ -479,15 +486,11 @@ class Tags:
return False
def to_path(self, ext: str) -> str | None:
def to_path(self, style: PathStyle, ext: str) -> str | None:
is_artist_item = self.is_artist_item(ext)
artist = self.albumartist or self.artist
if not (artist and self.album and self.title and ext or is_artist_item):
return None
artist = clean_fields_for_fs(artist, single_fields=False)
artist = clean_fields_for_fs(self.albumartist or self.artist, single_fields=False)
album = clean_fields_for_fs(self.album)
trackno = self.tracknumber and clean_fields_for_fs(self.tracknumber)
trackno = clean_fields_for_fs(self.tracknumber)
trackno = [f"{trackno:>02}"] if trackno else []
if self.artist and self.albumartist == [ "Various Artists" ] \
and self.artist != [ "Various Artists" ] \
@@ -501,6 +504,9 @@ class Tags:
if is_artist_item:
return os.path.join(artist, filename)
if style == PathStyle.Video:
return os.path.join(artist, filename)
elif style == PathStyle.Music:
if not (artist and album and title):
logger.warning(f"missing artist/album/title after cleaning path: {self} ({artist!r}, {album!r}, {title!r}")
return None
@@ -512,6 +518,8 @@ class Tags:
return os.path.join(producer, album, filename)
else:
return os.path.join(artist, album, filename)
else:
assert False, f"unknown PathStyle: {style}"
@staticmethod
def from_path(p: str) -> 'Tags':
@@ -922,9 +930,9 @@ class Tagger:
if self.guard_dry_run("writing tags"):
file_.write_tags(new_tags)
def fix_path(self, file_: MediaFile) -> None:
def fix_path(self, file_: MediaFile, style: PathStyle) -> None:
tags = self.tags_for(file_)
new_path = tags.to_path(os.path.splitext(file_.path_)[1])
new_path = tags.to_path(style, os.path.splitext(file_.path_)[1])
if new_path is None:
logger.debug(f"skipping untagged file: {file_.path_}")
logger.debug(f" {tags}")
@@ -1036,6 +1044,7 @@ def main():
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', default=False, help="apply tags already existing in one file (e.g. album tag) to adjacent files in the set")
parser.add_argument('--style', type=PathStyle, default=PathStyle.Music, help="how files should be organized into folders/paths")
parser.add_argument('--type', type=MediaType, help="only apply operation to a specific type of media")
subparsers = parser.add_subparsers(help="what to do")
@@ -1080,6 +1089,7 @@ def main():
gatherer = Gatherer(args.path, args.type, tags_provider)
tagger = Tagger(dry_run=args.dry_run, tags_provider=tags_provider)
style = args.style
if args.subcommand == "show":
for f in gatherer.files():
@@ -1093,7 +1103,7 @@ def main():
tagger.tag_file(f)
elif args.subcommand == "fix_paths":
for f in gatherer.files():
tagger.fix_path(f)
tagger.fix_path(f, style)
else:
assert False, f"unrecognized command {args.subcommand}"