feat: add FLAC lossless format support and refactor quality pickers (while staying backward compatible)

- Add `FLAC_FLAC` (16), `FLAC_FLAC_24BIT` (22) format codes (based on librespot-org/librespot#796, librespot-org/librespot#1424)
- Regenerate Metadata_pb2.py with protoc 3.20.1
- Add enums: `SuperAudioFormat.FLAC`, `AudioQuality.LOSSLESS`
- Refactor to generic DRY `FormatOnlyAudioQuality` base class while maintaining existing `VorbisOnlyAudioQuality` as wrapper
- Remove `AAC_24_NORM` (replaced by `FLAC_FLAC` at code 16)
This commit is contained in:
dakba
2025-10-10 20:08:41 -04:00
parent ee2c1107f6
commit e909cfdc76
4 changed files with 142 additions and 3705 deletions

View File

@@ -12,13 +12,13 @@ class AudioQuality(enum.Enum):
NORMAL = 0x00
HIGH = 0x01
VERY_HIGH = 0x02
LOSSLESS = 0x03
@staticmethod
def get_quality(audio_format: AudioFile.Format) -> AudioQuality:
if audio_format in [
AudioFile.MP3_96,
AudioFile.OGG_VORBIS_96,
AudioFile.AAC_24_NORM,
]:
return AudioQuality.NORMAL
if audio_format in [
@@ -35,6 +35,11 @@ class AudioQuality(enum.Enum):
AudioFile.AAC_48,
]:
return AudioQuality.VERY_HIGH
if audio_format in [
AudioFile.FLAC_FLAC,
AudioFile.FLAC_FLAC_24BIT,
]:
return AudioQuality.LOSSLESS
raise RuntimeError("Unknown format: {}".format(audio_format))
def get_matches(self,
@@ -47,35 +52,71 @@ class AudioQuality(enum.Enum):
return file_list
class VorbisOnlyAudioQuality(AudioQualityPicker):
logger = logging.getLogger("Librespot:Player:VorbisOnlyAudioQuality")
preferred: AudioQuality
class FormatOnlyAudioQuality(AudioQualityPicker):
# Generic quality picker; filters files by container format
def __init__(self, preferred: AudioQuality):
logger = logging.getLogger("Librespot:Player:FormatOnlyAudioQuality")
preferred: AudioQuality
format_filter: SuperAudioFormat
def __init__(self, preferred: AudioQuality, format_filter: SuperAudioFormat):
self.preferred = preferred
self.format_filter = format_filter
@staticmethod
def get_vorbis_file(files: typing.List[Metadata.AudioFile]):
def get_file_by_format(files: typing.List[Metadata.AudioFile],
format_type: SuperAudioFormat) -> typing.Optional[Metadata.AudioFile]:
for file in files:
if file.HasField("format") and SuperAudioFormat.get(
file.format) == SuperAudioFormat.VORBIS:
file.format) == format_type:
return file
return None
def get_file(self, files: typing.List[Metadata.AudioFile]):
matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(
files)
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
matches)
if vorbis is None:
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
files)
if vorbis is not None:
def get_file(self, files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
quality_matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(files)
selected_file = self.get_file_by_format(quality_matches, self.format_filter)
if selected_file is None:
# Try using any file matching the format, regardless of quality
selected_file = self.get_file_by_format(files, self.format_filter)
if selected_file is not None:
# Found format match (different quality than preferred)
self.logger.warning(
"Using {} because preferred {} couldn't be found.".format(
Metadata.AudioFile.Format.Name(vorbis.format),
self.preferred))
"Using {} format file with {} quality because preferred {} quality couldn't be found.".format(
self.format_filter.name,
AudioQuality.get_quality(selected_file.format).name,
self.preferred.name))
else:
available_formats = [SuperAudioFormat.get(f.format).name
for f in files if f.HasField("format")]
self.logger.fatal(
"Couldn't find any Vorbis file, available: {}")
return vorbis
"Couldn't find any {} file. Available formats: {}".format(
self.format_filter.name,
", ".join(set(available_formats)) if available_formats else "none"))
return selected_file
# Backward-compatible wrapper classes
class VorbisOnlyAudioQuality(FormatOnlyAudioQuality):
logger = logging.getLogger("Librespot:Player:VorbisOnlyAudioQuality")
def __init__(self, preferred: AudioQuality):
super().__init__(preferred, SuperAudioFormat.VORBIS)
@staticmethod
def get_vorbis_file(files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
return FormatOnlyAudioQuality.get_file_by_format(files, SuperAudioFormat.VORBIS)
class LosslessOnlyAudioQuality(FormatOnlyAudioQuality):
logger = logging.getLogger("Librespot:Player:LosslessOnlyAudioQuality")
def __init__(self, preferred: AudioQuality):
super().__init__(preferred, SuperAudioFormat.FLAC)
@staticmethod
def get_flac_file(files: typing.List[Metadata.AudioFile]) -> typing.Optional[Metadata.AudioFile]:
return FormatOnlyAudioQuality.get_file_by_format(files, SuperAudioFormat.FLAC)

View File

@@ -6,6 +6,7 @@ class SuperAudioFormat(enum.Enum):
MP3 = 0x00
VORBIS = 0x01
AAC = 0x02
FLAC = 0x03
@staticmethod
def get(audio_format: Metadata.AudioFile.Format):
@@ -26,7 +27,11 @@ class SuperAudioFormat(enum.Enum):
if audio_format in [
Metadata.AudioFile.Format.AAC_24,
Metadata.AudioFile.Format.AAC_48,
Metadata.AudioFile.Format.AAC_24_NORM,
]:
return SuperAudioFormat.AAC
if audio_format in [
Metadata.AudioFile.Format.FLAC_FLAC,
Metadata.AudioFile.Format.FLAC_FLAC_24BIT,
]:
return SuperAudioFormat.FLAC
raise RuntimeError("Unknown audio format: {}".format(audio_format))

File diff suppressed because one or more lines are too long