- Implemented a new beets plugin to import and manage music videos, supporting various video formats and providing metadata from IMVDB and MusicBrainz for autotagging. - Added installation instructions and configuration options in README.md. - Created IMVDBApi client for interacting with the IMVDB API. - Defined typing for various API responses and utility functions for mapping MusicBrainz data to beets TrackInfo. - Included desktop entry and JSON info files for a video titled "[SAST] The Evolution of Search-based Software Testing". - Added utility functions for handling artist credits and related metadata. - Introduced a grabbed.txt file for tracking video sources.
229 lines
7.2 KiB
Python
229 lines
7.2 KiB
Python
from __future__ import annotations
|
|
|
|
import beets
|
|
from urllib.parse import urljoin
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from beets.autotag.hooks import TrackInfo
|
|
from .._typing import JSONDict
|
|
|
|
MB_BASE_URL = "https://musicbrainz.org/"
|
|
|
|
|
|
def _artist_ids(credit: list[JSONDict]) -> list[str]:
|
|
"""
|
|
Given a list representing an ``artist-credit``,
|
|
return a list of artist IDs
|
|
"""
|
|
artist_ids: list[str] = []
|
|
for el in credit:
|
|
artist_ids.append(el["artist"]["id"])
|
|
|
|
return artist_ids
|
|
|
|
|
|
def _get_related_artist_names(relations: list[JSONDict], relation_type: str) -> str:
|
|
"""Given a list representing the artist relationships extract the names of
|
|
the remixers and concatenate them.
|
|
"""
|
|
related_artists: list[str] = []
|
|
|
|
for relation in relations:
|
|
if relation["type"] == relation_type:
|
|
related_artists.append(relation["artist"]["name"])
|
|
|
|
return ", ".join(related_artists)
|
|
|
|
|
|
def track_url(trackid: str) -> str:
|
|
return urljoin(MB_BASE_URL, f"recording/{trackid}")
|
|
|
|
|
|
def _preferred_alias(
|
|
aliases: list[JSONDict], languages: list[str] | None = None
|
|
) -> JSONDict | None:
|
|
"""Given a list of alias structures for an artist credit, select
|
|
and return the user's preferred alias or None if no matching
|
|
"""
|
|
if not aliases:
|
|
return None
|
|
|
|
# Only consider aliases that have locales set.
|
|
valid_aliases = [a for a in aliases if "locale" in a]
|
|
|
|
# Get any ignored alias types and lower case them to prevent case issues
|
|
ignored_alias_types = beets.config["import"]["ignored_alias_types"].as_str_seq()
|
|
ignored_alias_types = [a.lower() for a in ignored_alias_types]
|
|
|
|
# Search configured locales in order.
|
|
if languages is None:
|
|
languages = beets.config["import"]["languages"].as_str_seq()
|
|
|
|
for locale in languages:
|
|
# Find matching primary aliases for this locale that are not
|
|
# being ignored
|
|
matches: list[JSONDict] = []
|
|
for alias in valid_aliases:
|
|
if (
|
|
alias["locale"] == locale
|
|
and alias.get("primary")
|
|
and (alias.get("type") or "").lower() not in ignored_alias_types
|
|
):
|
|
matches.append(alias)
|
|
|
|
# Skip to the next locale if we have no matches
|
|
if not matches:
|
|
continue
|
|
|
|
return matches[0]
|
|
|
|
return None
|
|
|
|
|
|
def _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]:
|
|
"""Given a list representing an ``artist-credit`` block, flatten the
|
|
data into a triple of joined artist name strings: canonical, sort, and
|
|
credit.
|
|
"""
|
|
artist_parts, artist_sort_parts, artist_credit_parts = _multi_artist_credit(
|
|
credit, include_join_phrase=True
|
|
)
|
|
return (
|
|
"".join(artist_parts),
|
|
"".join(artist_sort_parts),
|
|
"".join(artist_credit_parts),
|
|
)
|
|
|
|
|
|
def _multi_artist_credit(
|
|
credit: list[JSONDict], include_join_phrase: bool
|
|
) -> tuple[list[str], list[str], list[str]]:
|
|
"""Given a list representing an ``artist-credit`` block, accumulate
|
|
data into a triple of joined artist name lists: canonical, sort, and
|
|
credit.
|
|
"""
|
|
artist_parts: list[str] = []
|
|
artist_sort_parts: list[str] = []
|
|
artist_credit_parts: list[str] = []
|
|
for el in credit:
|
|
alias = _preferred_alias(el["artist"].get("aliases", ()))
|
|
|
|
# An artist.
|
|
if alias:
|
|
cur_artist_name = alias["name"]
|
|
else:
|
|
cur_artist_name = el["artist"]["name"]
|
|
artist_parts.append(cur_artist_name)
|
|
|
|
# Artist sort name.
|
|
if alias:
|
|
artist_sort_parts.append(alias["sort-name"])
|
|
elif "sort-name" in el["artist"]:
|
|
artist_sort_parts.append(el["artist"]["sort-name"])
|
|
else:
|
|
artist_sort_parts.append(cur_artist_name)
|
|
|
|
# Artist credit.
|
|
if "name" in el:
|
|
artist_credit_parts.append(el["name"])
|
|
else:
|
|
artist_credit_parts.append(cur_artist_name)
|
|
|
|
if include_join_phrase and (joinphrase := el.get("joinphrase")):
|
|
artist_parts.append(joinphrase)
|
|
artist_sort_parts.append(joinphrase)
|
|
artist_credit_parts.append(joinphrase)
|
|
|
|
return (
|
|
artist_parts,
|
|
artist_sort_parts,
|
|
artist_credit_parts,
|
|
)
|
|
|
|
|
|
def map_mb_to_trackinfo(
|
|
recording: JSONDict,
|
|
index: int | None = None,
|
|
medium: int | None = None,
|
|
medium_index: int | None = None,
|
|
medium_total: int | None = None,
|
|
) -> TrackInfo:
|
|
info = beets.autotag.hooks.TrackInfo(
|
|
title=recording["title"],
|
|
track_id=recording["id"],
|
|
index=index,
|
|
medium=medium,
|
|
medium_index=medium_index,
|
|
medium_total=medium_total,
|
|
data_source="musicbrainz",
|
|
data_url=track_url(recording["id"]),
|
|
)
|
|
|
|
info.trackdisambig = recording.get("disambiguation")
|
|
lyricist: list[str] = []
|
|
composer: list[str] = []
|
|
composer_sort: list[str] = []
|
|
|
|
if recording.get("artist-credit"):
|
|
(info.artist, info.artist_sort, info.artist_credit) = _flatten_artist_credit(
|
|
recording["artist-credit"]
|
|
)
|
|
(
|
|
info.artists,
|
|
info.artists_sort,
|
|
info.artists_credit,
|
|
) = _multi_artist_credit(recording["artist-credit"], include_join_phrase=False)
|
|
|
|
info.artists_ids = _artist_ids(recording["artist-credit"])
|
|
info.artist_id = info.artists_ids[0]
|
|
|
|
if recording.get("artist-relations"):
|
|
info.remixer = _get_related_artist_names(
|
|
recording["artist-relations"], relation_type="remixer"
|
|
)
|
|
|
|
if recording.get("length"):
|
|
info.length = int(recording["length"]) / 1000.0
|
|
|
|
if recording.get("isrcs"):
|
|
info.isrc = ";".join(recording["isrcs"])
|
|
|
|
for work_relation in recording.get("work-relations", ()):
|
|
if work_relation["type"] != "performance":
|
|
continue
|
|
info.work = work_relation["work"]["title"]
|
|
info.mb_workid = work_relation["work"]["id"]
|
|
if "disambiguation" in work_relation["work"]:
|
|
info.work_disambig = work_relation["work"]["disambiguation"]
|
|
|
|
for artist_relation in work_relation["work"].get("artist-relations", ()):
|
|
if "type" in artist_relation:
|
|
type = artist_relation["type"]
|
|
if type == "lyricist":
|
|
lyricist.append(artist_relation["artist"]["name"])
|
|
elif type == "composer":
|
|
composer.append(artist_relation["artist"]["name"])
|
|
composer_sort.append(artist_relation["artist"]["sort-name"])
|
|
if lyricist:
|
|
info.lyricist = ", ".join(lyricist)
|
|
if composer:
|
|
info.composer = ", ".join(composer)
|
|
info.composer_sort = ", ".join(composer_sort)
|
|
|
|
arranger: list[str] = []
|
|
for artist_relation in recording.get("artist-relations", ()):
|
|
if "type" in artist_relation:
|
|
type = artist_relation["type"]
|
|
if type == "arranger":
|
|
arranger.append(artist_relation["artist"]["name"])
|
|
if arranger:
|
|
info.arranger = ", ".join(arranger)
|
|
|
|
# Supplementary fields provided by plugins
|
|
extra_trackdatas = beets.plugins.send("mb_track_extract", data=recording)
|
|
for extra_trackdata in extra_trackdatas:
|
|
info.update(extra_trackdata)
|
|
|
|
return info
|