swingmusic/app/lib/taglib.py
mungai-njoroge 38650a1a7e Refactor file: taglib.py and utils/parsers.py
Add dataclass and pathlib imports in taglib.py

Add clean_filename, ParseData, and extract_artist_title functions in
taglib.py

Modify get_tags function in taglib.py to use extract_artist_title
function

Modify LyricsProvider class in lyrics.py to add timeout parameter in
session.get

Remove parse_artist_from_filename and parse_title_from_filename
functions in utils/parsers.py

Add split_artists function in utils/parsers.py

Remove parse_artist_from_filename and parse_title_from_filename
functions in utils/parsers.py

Add remove_prod function in utils/parsers.py

Refactor code and improve code readability
2023-11-22 15:44:31 +03:00

253 lines
6.5 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.filetype = filetype
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