Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion streamrip/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class DatabaseConfig:
@dataclass(slots=True)
class ConversionConfig:
enabled: bool
# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC
# FLAC, ALAC, AIFF, OPUS, MP3, VORBIS, or AAC
codec: str
# In Hz. Tracks are downsampled if their sampling rate is greater than this.
# Value of 48000 is recommended to maximize quality and minimize space
Expand Down
2 changes: 1 addition & 1 deletion streamrip/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ failed_downloads_path = ""
# Convert tracks to a codec after downloading them.
[conversion]
enabled = false
# FLAC, ALAC, OPUS, MP3, VORBIS, or AAC
# FLAC, ALAC, AIFF, OPUS, MP3, VORBIS, or AAC
codec = "ALAC"
# In Hz. Tracks are downsampled if their sampling rate is greater than this.
# Value of 48000 is recommended to maximize quality and minimize space
Expand Down
10 changes: 10 additions & 0 deletions streamrip/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ class FLAC(Converter):
lossless = True


class AIFF(Converter):
"""Class for AIFF converter (lossless PCM)."""

codec_name = "aiff"
codec_lib = "pcm_s24be"
container = "aiff"
lossless = True


class LAME(Converter):
"""Class for libmp3lame converter.

Expand Down Expand Up @@ -288,5 +297,6 @@ def get(codec: str) -> type[Converter]:
"VORBIS": Vorbis,
"AAC": AAC,
"M4A": AAC,
"AIFF": AIFF,
}
return converter_classes[codec.upper()]
7 changes: 7 additions & 0 deletions streamrip/media/track.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ async def _convert(self):
)
await engine.convert()
self.download_path = engine.final_fn # because the extension changed
# Re-tag the converted file: ffmpeg does not reliably carry over all
# metadata (and some containers, e.g. AIFF, are not tagged at all).
# Only formats that tag_file understands are re-tagged; others (opus,
# ogg, ...) keep the metadata ffmpeg copied during conversion.
ext = self.download_path.split(".")[-1].lower()
if ext in ("flac", "m4a", "mp3", "aiff", "aif"):
await tag_file(self.download_path, self.meta, self.cover_path)

def _set_download_path(self):
c = self.config.session.filepaths
Expand Down
17 changes: 15 additions & 2 deletions streamrip/metadata/tagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import aiofiles
from mutagen import id3
from mutagen.aiff import AIFF
from mutagen.flac import FLAC, Picture
from mutagen.id3 import (
APIC, # type: ignore
Expand Down Expand Up @@ -100,12 +101,18 @@ class Container(Enum):
FLAC = 1
AAC = 2
MP3 = 3
AIFF = 4

def get_mutagen_class(self, path: str):
if self == Container.FLAC:
return FLAC(path)
elif self == Container.AAC:
return MP4(path)
elif self == Container.AIFF:
audio = AIFF(path)
if audio.tags is None:
audio.add_tags()
return audio.tags
elif self == Container.MP3:
try:
return ID3(path)
Expand All @@ -117,7 +124,7 @@ def get_mutagen_class(self, path: str):
def get_tag_pairs(self, meta) -> list[tuple]:
if self == Container.FLAC:
return self._tag_flac(meta)
elif self == Container.MP3:
elif self == Container.MP3 or self == Container.AIFF:
return self._tag_mp3(meta)
elif self == Container.AAC:
return self._tag_mp4(meta)
Expand Down Expand Up @@ -217,7 +224,7 @@ async def embed_cover(self, audio, cover_path):
async with aiofiles.open(cover_path, "rb") as img:
cover.data = await img.read()
audio.add_picture(cover)
elif self == Container.MP3:
elif self == Container.MP3 or self == Container.AIFF:
cover = APIC()
cover.type = 3
cover.mime = "image/jpeg"
Expand All @@ -236,6 +243,10 @@ def save_audio(self, audio, path):
audio.save()
elif self == Container.MP3:
audio.save(path, "v2_version=3")
elif self == Container.AIFF:
# AIFF uses an _IFFID3 tag object whose save() signature differs
# from MP3's ID3.save(); call it with defaults (v2_version=4).
audio.save(path)


async def tag_file(path: str, meta: TrackMetadata, cover_path: str | None):
Expand All @@ -246,6 +257,8 @@ async def tag_file(path: str, meta: TrackMetadata, cover_path: str | None):
container = Container.AAC
elif ext == "mp3":
container = Container.MP3
elif ext in ("aiff", "aif"):
container = Container.AIFF
else:
raise Exception(f"Invalid extension {ext}")

Expand Down
4 changes: 2 additions & 2 deletions streamrip/rip/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def wrapper(*args, **kwargs):
@click.option(
"-c",
"--codec",
help="Convert the downloaded files to an audio codec (ALAC, FLAC, MP3, AAC, or OGG)",
help="Convert the downloaded files to an audio codec (ALAC, FLAC, AIFF, MP3, AAC, or OGG)",
)
@click.option(
"--no-progress",
Expand Down Expand Up @@ -152,7 +152,7 @@ def rip(

if codec is not None:
c.session.conversion.enabled = True
assert codec.upper() in ("ALAC", "FLAC", "OGG", "MP3", "AAC")
assert codec.upper() in ("ALAC", "FLAC", "AIFF", "OGG", "MP3", "AAC")
c.session.conversion.codec = codec.upper()

if no_progress:
Expand Down