swingmusic/app/lib/taglib.py

252 lines
6.4 KiB
Python

from dataclasses import dataclass
import os
from io import BytesIO
from pathlib import Path
import re
import pendulum
from PIL import Image, UnidentifiedImageError
from tinytag import TinyTag
from app.settings import Defaults, Paths
from app.utils.hashing import create_hash
from app.utils.parsers import split_artists
from app.utils.wintools import win_replace_slash
def parse_album_art(filepath: str):
"""
Returns the album art for a given audio file.
"""
try:
tags = TinyTag.get(filepath, image=True)
return tags.get_image()
except: # pylint: disable=bare-except
return None
def extract_thumb(filepath: str, webp_path: str, overwrite=False) -> bool:
"""
Extracts the thumbnail from an audio file.
Returns the path to the thumbnail.
"""
original_img_path = os.path.join(Paths.get_original_thumb_path(), webp_path)
lg_img_path = os.path.join(Paths.get_lg_thumb_path(), webp_path)
sm_img_path = os.path.join(Paths.get_sm_thumb_path(), webp_path)
tsize = Defaults.THUMB_SIZE
sm_tsize = Defaults.SM_THUMB_SIZE
def save_image(img: Image.Image):
width, height = img.size
ratio = width / height
img.save(original_img_path, "webp")
img.resize((tsize, int(tsize / ratio)), Image.ANTIALIAS).save(
lg_img_path, "webp"
)
img.resize((sm_tsize, int(sm_tsize / ratio)), Image.ANTIALIAS).save(
sm_img_path, "webp"
)
if not overwrite and os.path.exists(sm_img_path):
img_size = os.path.getsize(sm_img_path)
if img_size > 0:
return True
album_art = parse_album_art(filepath)
if album_art is not None:
try:
img = Image.open(BytesIO(album_art))
except (UnidentifiedImageError, OSError):
return False
try:
save_image(img)
except OSError:
try:
png = img.convert("RGB")
save_image(png)
except: # pylint: disable=bare-except
return False
return True
return False
def parse_date(date_str: str | None) -> int | None:
"""
Extracts the date from a string and returns a timestamp.
"""
try:
date = pendulum.parse(date_str, strict=False)
return int(date.timestamp())
except Exception as e:
return None
def clean_filename(filename: str):
if "official" in filename.lower():
return re.sub(r"\s*\([^)]*official[^)]*\)", "", filename, flags=re.IGNORECASE)
return filename
@dataclass
class ParseData:
artist: str
title: str
def __post_init__(self):
self.artist = split_artists(self.artist)
def extract_artist_title(filename: str):
path = Path(filename).with_suffix("")
path = clean_filename(str(path))
split_result = path.split(" - ")
split_result = [x.strip() for x in split_result]
if len(split_result) == 1:
return ParseData("", split_result[0])
if len(split_result) > 2:
try:
int(split_result[0])
return ParseData(split_result[1], " - ".join(split_result[2:]))
except ValueError:
pass
artist = split_result[0]
title = split_result[1]
return ParseData(artist, title)
def get_tags(filepath: str):
"""
Returns the tags for a given audio file.
"""
filetype = filepath.split(".")[-1]
filename = (filepath.split("/")[-1]).replace(f".{filetype}", "")
try:
last_mod = round(os.path.getmtime(filepath))
except FileNotFoundError:
return None
try:
tags = TinyTag.get(filepath)
except: # noqa: E722
return None
no_albumartist: bool = (tags.albumartist == "") or (tags.albumartist is None)
no_artist: bool = (tags.artist == "") or (tags.artist is None)
if no_albumartist and not no_artist:
tags.albumartist = tags.artist
if no_artist and not no_albumartist:
tags.artist = tags.albumartist
parse_data = None
to_filename = ["title", "album"]
for tag in to_filename:
p = getattr(tags, tag)
if p == "" or p is None:
parse_data = extract_artist_title(filename)
title = parse_data.title
setattr(tags, tag, title)
parse = ["artist", "albumartist"]
for tag in parse:
p = getattr(tags, tag)
if p == "" or p is None:
if not parse_data:
parse_data = extract_artist_title(filename)
artist = parse_data.artist
if artist:
setattr(tags, tag, ", ".join(artist))
else:
setattr(tags, tag, "Unknown")
# TODO: Move parsing title, album and artist to startup. (Maybe!)
to_check = ["album", "year", "albumartist"]
for prop in to_check:
p = getattr(tags, prop)
if (p is None) or (p == ""):
setattr(tags, prop, "Unknown")
to_round = ["bitrate", "duration"]
for prop in to_round:
try:
setattr(tags, prop, round(getattr(tags, prop)))
except TypeError:
setattr(tags, prop, 0)
to_int = ["track", "disc"]
for prop in to_int:
try:
setattr(tags, prop, int(getattr(tags, prop)))
except (ValueError, TypeError):
setattr(tags, prop, 1)
try:
tags.copyright = tags.extra["copyright"]
except KeyError:
tags.copyright = None
tags.albumhash = create_hash(tags.album, tags.albumartist)
tags.trackhash = create_hash(tags.artist, tags.album, tags.title)
tags.image = f"{tags.albumhash}.webp"
tags.folder = win_replace_slash(os.path.dirname(filepath))
tags.date = parse_date(tags.year) or int(last_mod)
tags.filepath = win_replace_slash(filepath)
tags.last_mod = last_mod
tags.artists = tags.artist
tags.albumartists = tags.albumartist
# sub underscore with space
tags.title = tags.title.replace("_", " ")
tags.album = tags.album.replace("_", " ")
tags = tags.__dict__
# delete all tag properties that start with _ (tinytag internals)
for tag in list(tags):
if tag.startswith("_"):
del tags[tag]
to_delete = [
"filesize",
"audio_offset",
"channels",
"comment",
"composer",
"disc_total",
"extra",
"samplerate",
"track_total",
"year",
"bitdepth",
"artist",
"albumartist",
]
for tag in to_delete:
del tags[tag]
return tags