From 9277658fda8e307a182d4ba601fca9d728fcabce Mon Sep 17 00:00:00 2001 From: Colin Coe Date: Sat, 6 Jan 2024 19:03:06 -0700 Subject: [PATCH] Add fLaC metadata tag PERFORMER (just for pianists for now) (#36) * Changes to track downloading: Tidal reports 'Mix Engineer' instead of 'Mixer', so change in track.py: self.mixer: Optional[Tuple[str]] = self.get_contributors('Mix Engineer') Add support for the 'PERFORMER' tag in .flac files: for now, just 'piano' as returned by track.Track.get_credits() Change the order of tracks' ensuring that the audio stream is before video stream: copy to temp file THEN execute FFmpeg Remove a couple unused attributes from PlaylistsEndpointResponseJSON --- tidal_wave/models.py | 11 ++-------- tidal_wave/track.py | 50 +++++++++++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/tidal_wave/models.py b/tidal_wave/models.py index 5f8f9b6..cede4f4 100644 --- a/tidal_wave/models.py +++ b/tidal_wave/models.py @@ -296,9 +296,10 @@ def __post_init__(self): self.composer: Optional[Tuple[str]] = self.get_contributors("Composer") self.engineer: Optional[Tuple[str]] = self.get_contributors("Engineer") self.lyricist: Optional[Tuple[str]] = self.get_contributors("Lyricist") - self.mixer: Optional[Tuple[str]] = self.get_contributors("Mixer") + self.mixer: Optional[Tuple[str]] = self.get_contributors("Mix Engineer") self.producer: Optional[Tuple[str]] = self.get_contributors("Producer") self.remixer: Optional[Tuple[str]] = self.get_contributors("Remixer") + self.piano: Optional[Tuple[str]] = self.get_contributors("Piano") @dataclass(frozen=True) @@ -496,19 +497,11 @@ class PlaylistsEndpointResponseJSON(dataclass_wizard.JSONWizard): number_of_tracks: int number_of_videos: int description: str - last_updated: Annotated[ - datetime, dataclass_wizard.Pattern("%Y-%m-%dT%H:%M:%S.%f%z") - ] created: Annotated[datetime, dataclass_wizard.Pattern("%Y-%m-%dT%H:%M:%S.%f%z")] type: str public_playlist: bool url: str image: str # UUID v4 - popularity: int - square_image: str # UUID v4 - last_item_added_at: Annotated[ - datetime, dataclass_wizard.Pattern("%Y-%m-%dT%H:%M:%S.%f%z") - ] class TidalResource: diff --git a/tidal_wave/track.py b/tidal_wave/track.py index b2f03b4..828ad00 100644 --- a/tidal_wave/track.py +++ b/tidal_wave/track.py @@ -328,7 +328,7 @@ def craft_tags(self): tags[tag_map["track_peak_amplitude"]] = f"{self.metadata.peak}" tags[tag_map["track_replay_gain"]] = f"{self.metadata.replay_gain}" # credits - for tag in {"composer", "lyricist", "mixer", "producer", "remixer"}: + for tag in {"composer", "engineer", "lyricist", "mixer", "producer", "remixer"}: try: _credits_tag = ";".join(getattr(self.credits, tag)) except (TypeError, AttributeError): # NoneType problems @@ -343,12 +343,21 @@ def craft_tags(self): else: tags[tag_map["lyrics"]] = _lyrics - # track and disk if self.codec == "flac": + # track and disk tags["DISCTOTAL"] = f"{self.metadata.volume_number}" tags["DISC"] = f"{self.album.number_of_volumes}" tags["TRACKTOTAL"] = f"{self.album.number_of_tracks}" tags["TRACKNUMBER"] = f"{self.metadata.track_number}" + # instrument-specific + ## piano + try: + piano_credits: List[str] = [f"{pc} (piano)" for pc in self.credits.piano] + except (TypeError, AttributeError): # NoneType problems + pass + else: + tags["PERFORMER"] = piano_credits + elif self.codec == "m4a": # Have to convert to bytes the values of the tags starting with '----' for k, v in tags.copy().items(): @@ -381,24 +390,31 @@ def set_tags(self): self.mutagen["covr"] = [ MP4Cover(self.cover_path.read_bytes(), imageformat=MP4Cover.FORMAT_JPEG) ] - elif self.codec == "mka": - # FFmpeg chokes here with - # [matroska @ 0x5eb6a424f840] No wav codec tag found for codec none - # so DON'T attempt to add a cover image, and DON'T run the - # FFmpeg to put streams in order - self.mutagen.save() - return self.mutagen.save() # Make sure audio track comes first because of - # less-sophisticated audio players - with temporary_file(suffix=".mka") as tf: - cmd: List[str] = shlex.split( - f"""ffmpeg -hide_banner -loglevel quiet -y -i "{str(self.outfile.absolute())}" - -map 0:a:0 -map 0:v:0 -c copy "{tf.name}" """ - ) - subprocess.run(cmd) - shutil.copyfile(tf.name, str(self.outfile.absolute())) + # less-sophisticated audio players that only + # recognize the first stream + if self.codec == "flac": + with temporary_file(suffix=".mka") as tf: + shutil.move(str(self.outfile.absolute()), tf.name) + cmd: List[str] = shlex.split( + f"""ffmpeg -hide_banner -loglevel quiet -y -i "{tf.name}" + -map 0:a:0 -map 0:v:0 -c:a copy -c:v copy + -metadata:s:v title='Album cover' -metadata:s:v comment='Cover (front)' + -disposition:v attached_pic "{str(self.outfile.absolute())}" """ + ) + subprocess.run(cmd) + elif self.codec == "m4a": + with temporary_file(suffix=".mka") as tf: + shutil.move(str(self.outfile.absolute()), tf.name) + cmd: List[str] = shlex.split( + f"""ffmpeg -hide_banner -loglevel quiet -y -i "{tf.name}" + -map 0:a:0 -map 0:v:0 -c:a copy -c:v copy + "{str(self.outfile.absolute())}" """ + ) + subprocess.run(cmd) + def get( self,