Scan for audio properties, metadata, and embedded images in one single pass. Removed now useless lmsmetadata library + reworked code accordingly
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString containerTypeToString(ContainerType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ContainerType::AIFF:
|
||||
return "AIFF";
|
||||
case ContainerType::APE:
|
||||
return "APE";
|
||||
case ContainerType::ASF:
|
||||
return "ASF";
|
||||
case ContainerType::DSF:
|
||||
return "DSF";
|
||||
case ContainerType::FLAC:
|
||||
return "FLAC";
|
||||
case ContainerType::MP4:
|
||||
return "MP4";
|
||||
case ContainerType::MPC:
|
||||
return "MPC";
|
||||
case ContainerType::MPEG:
|
||||
return "MPEG";
|
||||
case ContainerType::Ogg:
|
||||
return "Ogg";
|
||||
case ContainerType::Shorten:
|
||||
return "Shorten";
|
||||
case ContainerType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case ContainerType::WAV:
|
||||
return "WAV";
|
||||
case ContainerType::WavPack:
|
||||
return "WavPack";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
core::LiteralString codecTypeToString(CodecType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CodecType::AAC:
|
||||
return "AAC";
|
||||
case CodecType::AC3:
|
||||
return "AC3";
|
||||
case CodecType::ALAC:
|
||||
return "ALAC";
|
||||
case CodecType::APE:
|
||||
return "APE";
|
||||
case CodecType::EAC3:
|
||||
return "EAC3";
|
||||
case CodecType::DSD:
|
||||
return "DSD";
|
||||
case CodecType::FLAC:
|
||||
return "FLAC";
|
||||
case CodecType::MP3:
|
||||
return "MP3";
|
||||
case CodecType::MP4ALS:
|
||||
return "MP4ALS";
|
||||
case CodecType::MPC7:
|
||||
return "MPC7";
|
||||
case CodecType::MPC8:
|
||||
return "MPC8";
|
||||
case CodecType::Opus:
|
||||
return "Opus";
|
||||
case CodecType::PCM:
|
||||
return "PCM";
|
||||
case CodecType::Shorten:
|
||||
return "Shorten";
|
||||
case CodecType::TrueAudio:
|
||||
return "TrueAudio";
|
||||
case CodecType::Vorbis:
|
||||
return "Vorbis";
|
||||
case CodecType::WavPack:
|
||||
return "WavPack";
|
||||
case CodecType::WMA1:
|
||||
return "WMA1";
|
||||
case CodecType::WMA2:
|
||||
return "WMA2";
|
||||
case CodecType::WMA9Pro:
|
||||
return "WMA9Pro";
|
||||
case CodecType::WMA9Lossless:
|
||||
return "WMA9Lossless";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString imageTypeToString(Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case Image::Type::Other:
|
||||
return "Other";
|
||||
case Image::Type::FileIcon:
|
||||
return "FileIcon";
|
||||
case Image::Type::OtherFileIcon:
|
||||
return "OtherFileIcon";
|
||||
case Image::Type::FrontCover:
|
||||
return "FrontCover";
|
||||
case Image::Type::BackCover:
|
||||
return "BackCover";
|
||||
case Image::Type::LeafletPage:
|
||||
return "LeafletPage";
|
||||
case Image::Type::Media:
|
||||
return "Media";
|
||||
case Image::Type::LeadArtist:
|
||||
return "LeadArtist";
|
||||
case Image::Type::Artist:
|
||||
return "Artist";
|
||||
case Image::Type::Conductor:
|
||||
return "Conductor";
|
||||
case Image::Type::Band:
|
||||
return "Band";
|
||||
case Image::Type::Composer:
|
||||
return "Composer";
|
||||
case Image::Type::Lyricist:
|
||||
return "Lyricist";
|
||||
case Image::Type::RecordingLocation:
|
||||
return "RecordingLocation";
|
||||
case Image::Type::DuringRecording:
|
||||
return "DuringRecording";
|
||||
case Image::Type::DuringPerformance:
|
||||
return "DuringPerformance";
|
||||
case Image::Type::MovieScreenCapture:
|
||||
return "MovieScreenCapture";
|
||||
case Image::Type::ColouredFish:
|
||||
return "ColouredFish";
|
||||
case Image::Type::Illustration:
|
||||
return "Illustration";
|
||||
case Image::Type::BandLogo:
|
||||
return "BandLogo";
|
||||
case Image::Type::PublisherLogo:
|
||||
return "PublisherLogo";
|
||||
case Image::Type::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "ffmpeg/AudioFileInfo.hpp"
|
||||
#include "ffmpeg/Utils.hpp"
|
||||
#include "taglib/AudioFileInfo.hpp"
|
||||
#include "taglib/Utils.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<IAudioFileInfo> parseAudioFile(const std::filesystem::path& p, const ParserOptions& parserOptions)
|
||||
{
|
||||
switch (parserOptions.parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return std::make_unique<taglib::AudioFileInfo>(p, parserOptions.readStyle, parserOptions.enableExtraDebugLogs);
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return std::make_unique<ffmpeg::AudioFileInfo>(p, parserOptions.enableExtraDebugLogs);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::span<const std::filesystem::path> getSupportedExtensions(ParserOptions::Parser parser)
|
||||
{
|
||||
switch (parser)
|
||||
{
|
||||
case ParserOptions::Parser::TagLib:
|
||||
return taglib::utils::getSupportedExtensions();
|
||||
case ParserOptions::Parser::FFmpeg:
|
||||
return ffmpeg::utils::getSupportedExtensions();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
core::LiteralString tagTypeToString(TagType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TagType::AcoustID:
|
||||
return "AcoustID";
|
||||
case TagType::AcoustIDFingerprint:
|
||||
return "AcoustIDFingerprint";
|
||||
case TagType::Advisory:
|
||||
return "Advisory";
|
||||
case TagType::Album:
|
||||
return "Album";
|
||||
case TagType::AlbumArtist:
|
||||
return "AlbumArtist";
|
||||
case TagType::AlbumArtists:
|
||||
return "AlbumArtists";
|
||||
case TagType::AlbumArtistSortOrder:
|
||||
return "AlbumArtistSortOrder";
|
||||
case TagType::AlbumArtistsSortOrder:
|
||||
return "AlbumArtistsSortOrder";
|
||||
case TagType::AlbumComment:
|
||||
return "AlbumComment";
|
||||
case TagType::AlbumSortOrder:
|
||||
return "AlbumSortOrder";
|
||||
case TagType::Arranger:
|
||||
return "Arranger";
|
||||
case TagType::Artist:
|
||||
return "Artist";
|
||||
case TagType::ArtistSortOrder:
|
||||
return "ArtistSortOrder";
|
||||
case TagType::Artists:
|
||||
return "Artists";
|
||||
case TagType::ArtistsSortOrder:
|
||||
return "ArtistsSortOrder";
|
||||
case TagType::ASIN:
|
||||
return "ASIN";
|
||||
case TagType::Barcode:
|
||||
return "Barcode";
|
||||
case TagType::BPM:
|
||||
return "BPM";
|
||||
case TagType::CatalogNumber:
|
||||
return "CatalogNumber";
|
||||
case TagType::Comment:
|
||||
return "Comment";
|
||||
case TagType::Compilation:
|
||||
return "Compilation";
|
||||
case TagType::Composer:
|
||||
return "Composer";
|
||||
case TagType::ComposerSortOrder:
|
||||
return "ComposerSortOrder";
|
||||
case TagType::Composers:
|
||||
return "Composers";
|
||||
case TagType::ComposersSortOrder:
|
||||
return "ComposersSortOrder";
|
||||
case TagType::Conductor:
|
||||
return "Conductor";
|
||||
case TagType::ConductorSortOrder:
|
||||
return "ConductorSortOrder";
|
||||
case TagType::Conductors:
|
||||
return "Conductors";
|
||||
case TagType::ConductorsSortOrder:
|
||||
return "ConductorsSortOrder";
|
||||
case TagType::Copyright:
|
||||
return "Copyright";
|
||||
case TagType::CopyrightURL:
|
||||
return "CopyrightURL";
|
||||
case TagType::Date:
|
||||
return "Date";
|
||||
case TagType::Director:
|
||||
return "Director";
|
||||
case TagType::DiscNumber:
|
||||
return "DiscNumber";
|
||||
case TagType::DiscSubtitle:
|
||||
return "DiscSubtitle";
|
||||
case TagType::EncodedBy:
|
||||
return "EncodedBy";
|
||||
case TagType::EncoderSettings:
|
||||
return "EncoderSettings";
|
||||
case TagType::EncodingTime:
|
||||
return "EncodingTime";
|
||||
case TagType::Engineer:
|
||||
return "Engineer";
|
||||
case TagType::GaplessPlayback:
|
||||
return "GaplessPlayback";
|
||||
case TagType::Genre:
|
||||
return "Genre";
|
||||
case TagType::Grouping:
|
||||
return "Grouping";
|
||||
case TagType::InitialKey:
|
||||
return "InitialKey";
|
||||
case TagType::ISRC:
|
||||
return "ISRC";
|
||||
case TagType::Language:
|
||||
return "Language";
|
||||
case TagType::Lyricist:
|
||||
return "Lyricist";
|
||||
case TagType::LyricistSortOrder:
|
||||
return "LyricistSortOrder";
|
||||
case TagType::Lyricists:
|
||||
return "Lyricists";
|
||||
case TagType::LyricistsSortOrder:
|
||||
return "LyricistsSortOrder";
|
||||
case TagType::Media:
|
||||
return "Media";
|
||||
case TagType::MixDJ:
|
||||
return "MixDJ";
|
||||
case TagType::Mixer:
|
||||
return "Mixer";
|
||||
case TagType::MixerSortOrder:
|
||||
return "MixerSortOrder";
|
||||
case TagType::Mixers:
|
||||
return "Mixers";
|
||||
case TagType::MixersSortOrder:
|
||||
return "MixersSortOrder";
|
||||
case TagType::Movement:
|
||||
return "Movement";
|
||||
case TagType::MovementCount:
|
||||
return "MovementCount";
|
||||
case TagType::MovementNumber:
|
||||
return "MovementNumber";
|
||||
case TagType::Mood:
|
||||
return "Mood";
|
||||
case TagType::MusicBrainzArtistID:
|
||||
return "MusicBrainzArtistID";
|
||||
case TagType::MusicBrainzArrangerID:
|
||||
return "MusicBrainzArrangerID";
|
||||
case TagType::MusicBrainzComposerID:
|
||||
return "MusicBrainzComposerID";
|
||||
case TagType::MusicBrainzConductorID:
|
||||
return "MusicBrainzConductorID";
|
||||
case TagType::MusicBrainzDirectorID:
|
||||
return "MusicBrainzDirectorID";
|
||||
case TagType::MusicBrainzDiscID:
|
||||
return "MusicBrainzDiscID";
|
||||
case TagType::MusicBrainzLyricistID:
|
||||
return "MusicBrainzLyricistID";
|
||||
case TagType::MusicBrainzProducerID:
|
||||
return "MusicBrainzProducerID";
|
||||
case TagType::MusicBrainzOriginalArtistID:
|
||||
return "MusicBrainzOriginalArtistID";
|
||||
case TagType::MusicBrainzRecordingID:
|
||||
return "MusicBrainzRecordingID";
|
||||
case TagType::MusicBrainzRemixerID:
|
||||
return "MusicBrainzRemixerID";
|
||||
case TagType::MusicBrainzWorkID:
|
||||
return "MusicBrainzWorkID";
|
||||
case TagType::Remixer:
|
||||
return "Remixer";
|
||||
case TagType::Script:
|
||||
return "Script";
|
||||
case TagType::ShowName:
|
||||
return "ShowName";
|
||||
case TagType::ShowNameSortOrder:
|
||||
return "ShowNameSortOrder";
|
||||
case TagType::ShowWorkAndMovement:
|
||||
return "ShowWorkAndMovement";
|
||||
case TagType::Subtitle:
|
||||
return "Subtitle";
|
||||
case TagType::TrackNumber:
|
||||
return "TrackNumber";
|
||||
case TagType::TrackTitle:
|
||||
return "TrackTitle";
|
||||
case TagType::TrackTitleSortOrder:
|
||||
return "TrackTitleSortOrder";
|
||||
case TagType::WorkTitle:
|
||||
return "WorkTitle";
|
||||
case TagType::Writer:
|
||||
return "Writer";
|
||||
case TagType::License:
|
||||
return "License";
|
||||
case TagType::MusicBrainzOriginalReleaseID:
|
||||
return "MusicBrainzOriginalReleaseID";
|
||||
case TagType::MusicBrainzMixerID:
|
||||
return "MusicBrainzMixerID";
|
||||
case TagType::MusicBrainzReleaseArtistID:
|
||||
return "MusicBrainzReleaseArtistID";
|
||||
case TagType::MusicBrainzReleaseGroupID:
|
||||
return "MusicBrainzReleaseGroupID";
|
||||
case TagType::MusicBrainzReleaseID:
|
||||
return "MusicBrainzReleaseID";
|
||||
case TagType::MusicBrainzTrackID:
|
||||
return "MusicBrainzTrackID";
|
||||
case TagType::MusicIPFingerprint:
|
||||
return "MusicIPFingerprint";
|
||||
case TagType::MusicIPPUID:
|
||||
return "MusicIPPUID";
|
||||
case TagType::OriginalAlbum:
|
||||
return "OriginalAlbum";
|
||||
case TagType::OriginalArtist:
|
||||
return "OriginalArtist";
|
||||
case TagType::OriginalFilename:
|
||||
return "OriginalFilename";
|
||||
case TagType::OriginalReleaseDate:
|
||||
return "OriginalReleaseDate";
|
||||
case TagType::OriginalReleaseYear:
|
||||
return "OriginalReleaseYear";
|
||||
case TagType::Podcast:
|
||||
return "Podcast";
|
||||
case TagType::PodcastURL:
|
||||
return "PodcastURL";
|
||||
case TagType::Producer:
|
||||
return "Producer";
|
||||
case TagType::ProducerSortOrder:
|
||||
return "ProducerSortOrder";
|
||||
case TagType::Producers:
|
||||
return "Producers";
|
||||
case TagType::ProducersSortOrder:
|
||||
return "ProducersSortOrder";
|
||||
case TagType::Rating:
|
||||
return "Rating";
|
||||
case TagType::RecordLabel:
|
||||
return "RecordLabel";
|
||||
case TagType::ReleaseCountry:
|
||||
return "ReleaseCountry";
|
||||
case TagType::ReleaseDate:
|
||||
return "ReleaseDate";
|
||||
case TagType::ReleaseStatus:
|
||||
return "ReleaseStatus";
|
||||
case TagType::ReleaseType:
|
||||
return "ReleaseType";
|
||||
case TagType::RemixerSortOrder:
|
||||
return "RemixerSortOrder";
|
||||
case TagType::Remixers:
|
||||
return "Remixers";
|
||||
case TagType::RemixersSortOrder:
|
||||
return "RemixersSortOrder";
|
||||
case TagType::ReplayGainAlbumGain:
|
||||
return "ReplayGainAlbumGain";
|
||||
case TagType::ReplayGainAlbumPeak:
|
||||
return "ReplayGainAlbumPeak";
|
||||
case TagType::ReplayGainAlbumRange:
|
||||
return "ReplayGainAlbumRange";
|
||||
case TagType::ReplayGainReferenceLoudness:
|
||||
return "ReplayGainReferenceLoudness";
|
||||
case TagType::ReplayGainTrackGain:
|
||||
return "ReplayGainTrackGain";
|
||||
case TagType::ReplayGainTrackPeak:
|
||||
return "ReplayGainTrackPeak";
|
||||
case TagType::ReplayGainTrackRange:
|
||||
return "ReplayGainTrackRange";
|
||||
case TagType::TotalDiscs:
|
||||
return "TotalDiscs";
|
||||
case TagType::TotalTracks:
|
||||
return "TotalTracks";
|
||||
case TagType::Website:
|
||||
return "Website";
|
||||
|
||||
case TagType::Count:
|
||||
break;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
} // namespace lms::audio
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#define __STDC_CONSTANT_MACROS
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/error.h>
|
||||
}
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string averror_to_string(int error)
|
||||
{
|
||||
std::array<char, 128> buf = { 0 };
|
||||
|
||||
if (::av_strerror(error, buf.data(), buf.size()) == 0)
|
||||
return buf.data();
|
||||
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
class AudioFileException : public AudioFileParsingException
|
||||
{
|
||||
public:
|
||||
AudioFileException(int avError)
|
||||
: AudioFileParsingException{ averror_to_string(avError) }
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||
{
|
||||
if (!dictionnary)
|
||||
return;
|
||||
|
||||
AVDictionaryEntry* tag = NULL;
|
||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
{
|
||||
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ContainerType> avdemuxerToContainerType(std::string_view name)
|
||||
{
|
||||
if (name == "aiff")
|
||||
return ContainerType::AIFF;
|
||||
if (name == "ape")
|
||||
return ContainerType::APE;
|
||||
if (name.starts_with("asf"))
|
||||
return ContainerType::ASF;
|
||||
if (name == "dsf")
|
||||
return ContainerType::DSF;
|
||||
if (name == "flac")
|
||||
return ContainerType::FLAC;
|
||||
if (name.find("mp4") != std::string_view::npos)
|
||||
return ContainerType::MP4;
|
||||
if (name.starts_with("mpc"))
|
||||
return ContainerType::MPC;
|
||||
if (name == "mp3")
|
||||
return ContainerType::MPEG;
|
||||
if (name == "ogg")
|
||||
return ContainerType::Ogg;
|
||||
if (name == "shn")
|
||||
return ContainerType::Shorten;
|
||||
if (name == "tta")
|
||||
return ContainerType::TrueAudio;
|
||||
if (name == "wav")
|
||||
return ContainerType::WAV;
|
||||
if (name == "wv")
|
||||
return ContainerType::WavPack;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<CodecType> avcodecToCodecType(AVCodecID codec)
|
||||
{
|
||||
switch (codec)
|
||||
{
|
||||
case AV_CODEC_ID_MP3:
|
||||
return CodecType::MP3;
|
||||
case AV_CODEC_ID_AAC:
|
||||
return CodecType::AAC;
|
||||
case AV_CODEC_ID_AC3:
|
||||
return CodecType::AC3;
|
||||
case AV_CODEC_ID_VORBIS:
|
||||
return CodecType::Vorbis;
|
||||
case AV_CODEC_ID_WMAV1:
|
||||
return CodecType::WMA1;
|
||||
case AV_CODEC_ID_WMAV2:
|
||||
return CodecType::WMA2;
|
||||
case AV_CODEC_ID_WMAPRO:
|
||||
return CodecType::WMA9Pro;
|
||||
case AV_CODEC_ID_WMALOSSLESS:
|
||||
return CodecType::WMA9Lossless;
|
||||
case AV_CODEC_ID_FLAC:
|
||||
return CodecType::FLAC;
|
||||
case AV_CODEC_ID_ALAC:
|
||||
return CodecType::ALAC;
|
||||
case AV_CODEC_ID_WAVPACK:
|
||||
return CodecType::WavPack;
|
||||
case AV_CODEC_ID_MUSEPACK7:
|
||||
return CodecType::MPC7;
|
||||
case AV_CODEC_ID_MUSEPACK8:
|
||||
return CodecType::MPC8;
|
||||
case AV_CODEC_ID_APE:
|
||||
return CodecType::APE;
|
||||
case AV_CODEC_ID_EAC3:
|
||||
return CodecType::EAC3;
|
||||
case AV_CODEC_ID_MP4ALS:
|
||||
return CodecType::MP4ALS;
|
||||
case AV_CODEC_ID_OPUS:
|
||||
return CodecType::Opus;
|
||||
case AV_CODEC_ID_SHORTEN:
|
||||
return CodecType::Shorten;
|
||||
case AV_CODEC_ID_DSD_LSBF:
|
||||
case AV_CODEC_ID_DSD_LSBF_PLANAR:
|
||||
case AV_CODEC_ID_DSD_MSBF:
|
||||
case AV_CODEC_ID_DSD_MSBF_PLANAR:
|
||||
return CodecType::DSD;
|
||||
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFile::AudioFile(const std::filesystem::path& p)
|
||||
: _p{ p }
|
||||
{
|
||||
int error{ avformat_open_input(&_context, _p.c_str(), nullptr, nullptr) };
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot open " << _p << ": " << averror_to_string(error));
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
|
||||
error = avformat_find_stream_info(_context, nullptr);
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Cannot find stream information on " << _p << ": " << averror_to_string(error));
|
||||
avformat_close_input(&_context);
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
}
|
||||
|
||||
AudioFile::~AudioFile()
|
||||
{
|
||||
avformat_close_input(&_context);
|
||||
}
|
||||
|
||||
const std::filesystem::path& AudioFile::getPath() const
|
||||
{
|
||||
return _p;
|
||||
}
|
||||
|
||||
ContainerInfo AudioFile::getContainerInfo() const
|
||||
{
|
||||
ContainerInfo info;
|
||||
|
||||
info.container = avdemuxerToContainerType(_context->iformat->name);
|
||||
info.containerName = _context->iformat->name;
|
||||
|
||||
info.bitrate = _context->bit_rate;
|
||||
info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1'000 };
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
AudioFile::MetadataMap AudioFile::getMetaData() const
|
||||
{
|
||||
MetadataMap res;
|
||||
|
||||
getMetaDataFromDictionnary(_context->metadata, res);
|
||||
|
||||
// HACK for OGG files
|
||||
// If we did not find tags, search metadata in streams
|
||||
if (res.empty())
|
||||
{
|
||||
for (std::size_t i{}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
|
||||
|
||||
if (!res.empty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<StreamInfo> AudioFile::getStreamInfo() const
|
||||
{
|
||||
std::vector<StreamInfo> res;
|
||||
|
||||
for (std::size_t i{}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
std::optional<StreamInfo> streamInfo{ getStreamInfo(i) };
|
||||
if (streamInfo)
|
||||
res.emplace_back(std::move(*streamInfo));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> AudioFile::getBestStreamIndex() const
|
||||
{
|
||||
int res = ::av_find_best_stream(_context,
|
||||
AVMEDIA_TYPE_AUDIO,
|
||||
-1, // Auto
|
||||
-1, // Auto
|
||||
NULL,
|
||||
0);
|
||||
|
||||
if (res < 0)
|
||||
return std::nullopt;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
|
||||
{
|
||||
std::optional<StreamInfo> res;
|
||||
|
||||
std::optional<std::size_t> bestStreamIndex{ getBestStreamIndex() };
|
||||
if (bestStreamIndex)
|
||||
res = getStreamInfo(*bestStreamIndex);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool AudioFile::hasAttachedPictures() const
|
||||
{
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
{
|
||||
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AudioFile::visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const
|
||||
{
|
||||
static const std::unordered_map<int, std::string> codecMimeMap{
|
||||
{ AV_CODEC_ID_BMP, "image/bmp" },
|
||||
{ AV_CODEC_ID_GIF, "image/gif" },
|
||||
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
|
||||
{ AV_CODEC_ID_PNG, "image/png" },
|
||||
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||
{
|
||||
AVStream* avstream = _context->streams[i];
|
||||
|
||||
// Skip attached pics
|
||||
if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
|
||||
continue;
|
||||
|
||||
if (avstream->codecpar == nullptr)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Skipping stream " << i << " since no codecpar is set");
|
||||
continue;
|
||||
}
|
||||
|
||||
MetadataMap metadata;
|
||||
getMetaDataFromDictionnary(avstream->metadata, metadata);
|
||||
|
||||
Picture picture;
|
||||
|
||||
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
|
||||
if (itMime != codecMimeMap.end())
|
||||
{
|
||||
picture.mimeType = itMime->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
picture.mimeType = "application/octet-stream";
|
||||
LMS_LOG(AUDIO, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
|
||||
}
|
||||
|
||||
const AVPacket& pkt{ avstream->attached_pic };
|
||||
|
||||
picture.data = std::span{ reinterpret_cast<const std::byte*>(pkt.data), static_cast<std::size_t>(pkt.size) };
|
||||
func(picture, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<StreamInfo> AudioFile::getStreamInfo(std::size_t streamIndex) const
|
||||
{
|
||||
std::optional<StreamInfo> res;
|
||||
|
||||
AVStream* avstream{ _context->streams[streamIndex] };
|
||||
assert(avstream);
|
||||
|
||||
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
return res;
|
||||
|
||||
if (!avstream->codecpar)
|
||||
{
|
||||
LMS_LOG(AUDIO, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
|
||||
return res;
|
||||
}
|
||||
|
||||
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
|
||||
return res;
|
||||
|
||||
res.emplace();
|
||||
|
||||
res->index = streamIndex;
|
||||
res->codec = avcodecToCodecType(avstream->codecpar->codec_id);
|
||||
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||
|
||||
if (avstream->codecpar->bit_rate)
|
||||
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
|
||||
if (avstream->codecpar->bits_per_coded_sample)
|
||||
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
|
||||
else if (avstream->codecpar->bits_per_raw_sample)
|
||||
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_raw_sample);
|
||||
|
||||
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
|
||||
if (avstream->codecpar->channels)
|
||||
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
|
||||
#else
|
||||
if (avstream->codecpar->ch_layout.nb_channels)
|
||||
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
|
||||
#endif
|
||||
assert(!res->codecName.empty()); // doc says it is never NULL
|
||||
if (avstream->codecpar->sample_rate)
|
||||
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
|
||||
|
||||
return res;
|
||||
}
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
|
||||
extern "C"
|
||||
{
|
||||
struct AVFormatContext;
|
||||
}
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
struct Picture
|
||||
{
|
||||
std::string mimeType;
|
||||
std::span<const std::byte> data; // valid as long as IAudioFile exists
|
||||
};
|
||||
|
||||
struct ContainerInfo
|
||||
{
|
||||
std::optional<ContainerType> container;
|
||||
std::string containerName;
|
||||
|
||||
std::size_t bitrate{};
|
||||
std::chrono::milliseconds duration{};
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t index{};
|
||||
std::optional<CodecType> codec;
|
||||
std::string codecName;
|
||||
|
||||
std::optional<size_t> bitrate;
|
||||
std::optional<std::size_t> bitsPerSample;
|
||||
std::optional<std::size_t> channelCount;
|
||||
std::optional<std::size_t> sampleRate;
|
||||
};
|
||||
|
||||
class AudioFile
|
||||
{
|
||||
public:
|
||||
AudioFile(const std::filesystem::path& p);
|
||||
~AudioFile();
|
||||
AudioFile(const AudioFile&) = delete;
|
||||
AudioFile& operator=(const AudioFile&) = delete;
|
||||
|
||||
using MetadataMap = std::unordered_map<std::string, std::string>;
|
||||
|
||||
const std::filesystem::path& getPath() const;
|
||||
ContainerInfo getContainerInfo() const;
|
||||
MetadataMap getMetaData() const;
|
||||
std::vector<StreamInfo> getStreamInfo() const;
|
||||
std::optional<StreamInfo> getBestStreamInfo() const;
|
||||
std::optional<std::size_t> getBestStreamIndex() const;
|
||||
bool hasAttachedPictures() const;
|
||||
void visitAttachedPictures(std::function<void(const Picture&, const MetadataMap&)> func) const;
|
||||
|
||||
private:
|
||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context{};
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
#include "ImageReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const AudioFile& audioFile)
|
||||
{
|
||||
AudioProperties audioProperties;
|
||||
|
||||
const auto containerInfo{ audioFile.getContainerInfo() };
|
||||
const auto bestStreamInfo{ audioFile.getBestStreamInfo() };
|
||||
if (!bestStreamInfo)
|
||||
throw AudioFileParsingException{ audioFile.getPath(), "Cannot find best audio stream" };
|
||||
|
||||
if (!containerInfo.container)
|
||||
throw AudioFileParsingException{ audioFile.getPath(), "Unhandled container type '" + containerInfo.containerName + "'" };
|
||||
|
||||
audioProperties.container = *containerInfo.container;
|
||||
audioProperties.duration = containerInfo.duration;
|
||||
audioProperties.codec = bestStreamInfo->codec;
|
||||
audioProperties.bitrate = bestStreamInfo->bitrate;
|
||||
audioProperties.bitsPerSample = bestStreamInfo->bitsPerSample;
|
||||
audioProperties.channelCount = bestStreamInfo->channelCount;
|
||||
audioProperties.sampleRate = bestStreamInfo->sampleRate;
|
||||
|
||||
return audioProperties;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs)
|
||||
: _audioFile{ std::make_unique<AudioFile>(filePath) }
|
||||
, _audioProperties{ std::make_unique<AudioProperties>(computeAudioProperties(*_audioFile)) }
|
||||
, _tagReader{ std::make_unique<TagReader>(*_audioFile, enableExtraDebugLogs) }
|
||||
, _imageReader{ std::make_unique<ImageReader>(*_audioFile) }
|
||||
{
|
||||
}
|
||||
|
||||
AudioFileInfo::~AudioFileInfo() = default;
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return *_audioProperties;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return *_imageReader;
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return *_tagReader;
|
||||
}
|
||||
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AudioFile;
|
||||
class TagReader;
|
||||
class ImageReader;
|
||||
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, bool enableExtraDebugLogs);
|
||||
~AudioFileInfo();
|
||||
AudioFileInfo(const AudioFileInfo&) = delete;
|
||||
AudioFileInfo& operator=(const AudioFileInfo&) = delete;
|
||||
|
||||
private:
|
||||
const AudioProperties& getAudioProperties() const override;
|
||||
const IImageReader& getImageReader() const override;
|
||||
const ITagReader& getTagReader() const override;
|
||||
|
||||
std::unique_ptr<AudioFile> _audioFile;
|
||||
std::unique_ptr<AudioProperties> _audioProperties;
|
||||
std::unique_ptr<TagReader> _tagReader;
|
||||
std::unique_ptr<ImageReader> _imageReader;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ImageReader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
ImageReader::ImageReader(const AudioFile& audioFile)
|
||||
: _audioFile{ audioFile }
|
||||
{
|
||||
}
|
||||
|
||||
ImageReader::~ImageReader() = default;
|
||||
|
||||
void ImageReader::visitImages(const ImageVisitor& visitor) const
|
||||
{
|
||||
auto metaDataHasKeyword{ [](const AudioFile::MetadataMap& metadata, std::string_view keyword) {
|
||||
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
|
||||
} };
|
||||
|
||||
_audioFile.visitAttachedPictures([&](const Picture& picture, const AudioFile::MetadataMap& metaData) {
|
||||
Image image;
|
||||
image.data = picture.data;
|
||||
image.mimeType = picture.mimeType;
|
||||
if (metaDataHasKeyword(metaData, "front"))
|
||||
image.type = Image::Type::FrontCover;
|
||||
else if (metaDataHasKeyword(metaData, "back"))
|
||||
image.type = Image::Type::BackCover;
|
||||
|
||||
visitor(image);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class AudioFile;
|
||||
|
||||
class ImageReader : public IImageReader
|
||||
{
|
||||
public:
|
||||
ImageReader(const AudioFile& audioFile);
|
||||
~ImageReader() override;
|
||||
|
||||
ImageReader(const ImageReader&) = delete;
|
||||
ImageReader& operator=(const ImageReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitImages(const ImageVisitor& visitor) const override;
|
||||
|
||||
const AudioFile& _audioFile;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright (C) 2013 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagReader.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Mapping to internal avformat names and/or common alternative custom names
|
||||
static const std::unordered_map<TagType, std::vector<std::string>> avFormatTagMapping{
|
||||
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
|
||||
{ TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } },
|
||||
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } },
|
||||
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
|
||||
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "MUSICBRAINZ/ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
|
||||
{ TagType::AlbumSortOrder, { "ALBUMSORT", "ALBUM-SORT" } },
|
||||
{ TagType::Arranger, { "ARRANGER" } },
|
||||
{ TagType::Artist, { "ARTIST" } },
|
||||
{ TagType::ArtistSortOrder, { "ARTISTSORT", "ARTIST-SORT", "WM/ARTISTSORTORDER" } },
|
||||
{ TagType::Artists, { "ARTISTS", "WM/ARTISTS" } },
|
||||
{ TagType::ArtistsSortOrder, { "ARTISTSSORT", "ARTISTS-SORT", "WM/ARTISTSSORTORDER" } },
|
||||
{ TagType::ASIN, { "ASIN" } },
|
||||
{ TagType::Barcode, { "BARCODE", "WM/BARCODE" } },
|
||||
{ TagType::BPM, { "BPM" } },
|
||||
{ TagType::CatalogNumber, { "CATALOGNUMBER", "WM/CATALOGNO" } },
|
||||
{ TagType::Comment, { "COMMENT" } },
|
||||
{ TagType::Compilation, { "COMPILATION", "TCMP" } },
|
||||
{ TagType::Composer, { "COMPOSER" } },
|
||||
{ TagType::Composers, { "COMPOSERS" } },
|
||||
{ TagType::ComposerSortOrder, { "COMPOSERSORT", "TSOC" } },
|
||||
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
|
||||
{ TagType::Conductor, { "CONDUCTOR" } },
|
||||
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
|
||||
{ TagType::Conductors, { "CONDUCTORS" } },
|
||||
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
|
||||
{ TagType::Copyright, { "COPYRIGHT" } },
|
||||
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
|
||||
{ TagType::Date, { "DATE", "YEAR", "WM/YEAR" } },
|
||||
{ TagType::Director, { "DIRECTOR" } },
|
||||
{ TagType::DiscNumber, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" } },
|
||||
{ TagType::DiscSubtitle, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" } },
|
||||
{ TagType::EncodedBy, { "ENCODEDBY" } },
|
||||
{ TagType::EncodingTime, { "ENCODINGTIME", "TDEN" } },
|
||||
{ TagType::Engineer, { "ENGINEER" } },
|
||||
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
|
||||
{ TagType::Genre, { "GENRE" } },
|
||||
{ TagType::Grouping, { "GROUPING", "WM/CONTENTGROUPDESCRIPTION", "ALBUMGROUPING" } },
|
||||
{ TagType::InitialKey, { "INITIALKEY" } },
|
||||
{ TagType::ISRC, { "ISRC", "WM/ISRC", "TSRC" } },
|
||||
{ TagType::Language, { "LANGUAGE" } },
|
||||
{ TagType::License, { "LICENSE" } },
|
||||
{ TagType::Lyricist, { "LYRICIST" } },
|
||||
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
|
||||
{ TagType::Lyricists, { "LYRICISTS" } },
|
||||
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
|
||||
{ TagType::Media, { "TMED", "MEDIA", "WM/MEDIA" } },
|
||||
{ TagType::MixDJ, { "DJMIXER" } },
|
||||
{ TagType::Mixer, { "MIXER" } },
|
||||
{ TagType::MixerSortOrder, { "MIXERSORT" } },
|
||||
{ TagType::Mixers, { "MIXERS" } },
|
||||
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
||||
{ TagType::Mood, { "MOOD" } },
|
||||
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
|
||||
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
|
||||
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
|
||||
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
||||
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
||||
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
||||
{ TagType::MusicBrainzConductorID, { "MUSICBRAINZ_CONDUCTORID", "MUSICBRAINZ CONDUCTOR ID", "MUSICBRAINZ/CONDUCTOR ID" } },
|
||||
{ TagType::MusicBrainzDirectorID, { "MUSICBRAINZ_DIRECTORID", "MUSICBRAINZ DIRECTOR ID", "MUSICBRAINZ/DIRECTOR ID" } },
|
||||
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
|
||||
{ TagType::MusicBrainzLyricistID, { "MUSICBRAINZ_LYRICISTID", "MUSICBRAINZ LYRICIST ID", "MUSICBRAINZ/LYRICIST ID" } },
|
||||
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
|
||||
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
|
||||
{ TagType::MusicBrainzMixerID, { "MUSICBRAINZ_MIXERID", "MUSICBRAINZ MIXER ID", "MUSICBRAINZ/MIXER ID" } },
|
||||
{ TagType::MusicBrainzProducerID, { "MUSICBRAINZ_PRODUCERID", "MUSICBRAINZ PRODUCER ID", "MUSICBRAINZ/PRODUCER ID" } },
|
||||
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
|
||||
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
|
||||
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
|
||||
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
|
||||
{ TagType::MusicBrainzRemixerID, { "MUSICBRAINZ_REMIXERID", "MUSICBRAINZ REMIXER ID", "MUSICBRAINZ/REMIXER ID" } },
|
||||
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
|
||||
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
|
||||
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
|
||||
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
|
||||
{ TagType::OriginalReleaseDate, { "ORIGINALDATE", "TDOR", "WM/ORIGINALRELEASETIME" } },
|
||||
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR", "TORY", "WM/ORIGINALRELEASEYEAR" } },
|
||||
{ TagType::Podcast, { "PODCAST" } },
|
||||
{ TagType::PodcastURL, { "PODCASTURL" } },
|
||||
{ TagType::Producer, { "PRODUCER" } },
|
||||
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
|
||||
{ TagType::Producers, { "PRODUCERS" } },
|
||||
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
|
||||
{ TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } },
|
||||
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
|
||||
{ TagType::ReleaseDate, { "RELEASEDATE" } },
|
||||
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
|
||||
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
|
||||
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
|
||||
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
|
||||
{ TagType::Remixers, { "REMIXERS" } },
|
||||
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
|
||||
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
|
||||
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
|
||||
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
|
||||
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
|
||||
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
|
||||
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
|
||||
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
|
||||
{ TagType::Script, { "SCRIPT", "WM/SCRIPT" } },
|
||||
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
|
||||
{ TagType::Subtitle, { "SUBTITLE" } },
|
||||
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS" } },
|
||||
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
|
||||
{ TagType::TrackNumber, { "TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER" } },
|
||||
{ TagType::TrackTitle, { "TITLE" } },
|
||||
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
|
||||
{ TagType::WorkTitle, { "WORK" } },
|
||||
{ TagType::Writer, { "WRITER" } },
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TagReader::TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs)
|
||||
: _audioFile{ audioFile }
|
||||
, _metaDataMap{ audioFile.getMetaData() }
|
||||
{
|
||||
if (enableExtraDebugLogs && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
|
||||
{
|
||||
for (const auto& [key, value] : _metaDataMap)
|
||||
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
|
||||
}
|
||||
}
|
||||
|
||||
TagReader::~TagReader() = default;
|
||||
|
||||
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(avFormatTagMapping))
|
||||
return;
|
||||
|
||||
for (const std::string& tagName : itTagNames->second)
|
||||
{
|
||||
bool visited{};
|
||||
|
||||
visitTagValues(tagName, [&](std::string_view value) {
|
||||
visited = true;
|
||||
visitor(value);
|
||||
});
|
||||
|
||||
if (visited)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itValues{ _metaDataMap.find(std::string{ key }) };
|
||||
if (itValues == std::cend(_metaDataMap))
|
||||
return;
|
||||
|
||||
visitor(itValues->second);
|
||||
}
|
||||
|
||||
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
{
|
||||
visitTagValues("PERFORMER", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
|
||||
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
{
|
||||
// MPEG files: need to visit LYRICS-language entries
|
||||
for (const auto& [tag, value] : _metaDataMap)
|
||||
{
|
||||
constexpr std::string_view lyricsPrefix{ "LYRICS-" };
|
||||
if (tag.starts_with(lyricsPrefix))
|
||||
{
|
||||
const std::string language{ core::stringUtils::stringToLower(tag.substr(lyricsPrefix.size())) };
|
||||
visitor(language, value);
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, just visit regular LYRICS tag with no language
|
||||
visitTagValues("LYRICS", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "AudioFile.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class TagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
TagReader(const AudioFile& audioFile, bool enableExtraDebugLogs);
|
||||
~TagReader() override;
|
||||
TagReader(const TagReader&) = delete;
|
||||
TagReader& operator=(const TagReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||
|
||||
const AudioFile& _audioFile;
|
||||
AudioFile::MetadataMap _metaDataMap;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Transcoder.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <iomanip>
|
||||
|
||||
#include "core/IChildProcessManager.hpp"
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
#include "audio/Exception.hpp"
|
||||
#include "audio/TranscodeTypes.hpp"
|
||||
|
||||
namespace lms::audio
|
||||
{
|
||||
std::unique_ptr<ITranscoder> createTranscoder(const TranscodeParameters& parameters)
|
||||
{
|
||||
return std::make_unique<ffmpeg::Transcoder>(parameters);
|
||||
}
|
||||
} // namespace lms::audio
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||
|
||||
static std::atomic<size_t> globalId{};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
|
||||
void Transcoder::init()
|
||||
{
|
||||
ffmpegPath = core::Service<core::IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||
if (!std::filesystem::exists(ffmpegPath))
|
||||
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
||||
}
|
||||
|
||||
Transcoder::Transcoder(const TranscodeParameters& parameters)
|
||||
: _debugId{ globalId++ }
|
||||
, _inputParams{ parameters.inputParameters }
|
||||
, _outputParams{ parameters.outputParameters }
|
||||
{
|
||||
start();
|
||||
}
|
||||
|
||||
Transcoder::~Transcoder() = default;
|
||||
|
||||
void Transcoder::start()
|
||||
{
|
||||
if (ffmpegPath.empty())
|
||||
init();
|
||||
|
||||
try
|
||||
{
|
||||
if (!std::filesystem::exists(_inputParams.filePath))
|
||||
throw Exception{ "File " + _inputParams.filePath.string() + " does not exist!" };
|
||||
if (!std::filesystem::is_regular_file(_inputParams.filePath))
|
||||
throw Exception{ "File " + _inputParams.filePath.string() + " is not regular!" };
|
||||
}
|
||||
catch (const std::filesystem::filesystem_error& e)
|
||||
{
|
||||
// TODO store/raise e.code()
|
||||
throw Exception{ "File error '" + _inputParams.filePath.string() + "': " + e.what() };
|
||||
}
|
||||
|
||||
LOG(INFO, "Transcoding file " << _inputParams.filePath);
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
args.emplace_back(ffmpegPath.string());
|
||||
|
||||
// Make sure:
|
||||
// - we do not produce anything in the stderr output
|
||||
// - we do not rely on input
|
||||
// in order not to block the whole forked process
|
||||
args.emplace_back("-loglevel");
|
||||
args.emplace_back("quiet");
|
||||
args.emplace_back("-nostdin");
|
||||
|
||||
// input Offset
|
||||
{
|
||||
args.emplace_back("-ss");
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_inputParams.offset.count() / float{ 1'000 });
|
||||
args.emplace_back(oss.str());
|
||||
}
|
||||
|
||||
// Input file
|
||||
args.emplace_back("-i");
|
||||
args.emplace_back(_inputParams.filePath.string());
|
||||
|
||||
if (_outputParams.stripMetadata)
|
||||
{
|
||||
// Strip metadata
|
||||
args.emplace_back("-map_metadata");
|
||||
args.emplace_back("-1");
|
||||
}
|
||||
|
||||
// Skip video flows (including covers)
|
||||
args.emplace_back("-vn");
|
||||
|
||||
// Output bitrates
|
||||
if (_outputParams.bitrate)
|
||||
{
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(*_outputParams.bitrate));
|
||||
}
|
||||
|
||||
// Codecs and formats
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("mp3");
|
||||
break;
|
||||
|
||||
case OutputFormat::OGG_OPUS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libopus");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
|
||||
case OutputFormat::MATROSKA_OPUS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libopus");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("matroska");
|
||||
break;
|
||||
|
||||
case OutputFormat::OGG_VORBIS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libvorbis");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("ogg");
|
||||
break;
|
||||
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
args.emplace_back("-acodec");
|
||||
args.emplace_back("libvorbis");
|
||||
args.emplace_back("-f");
|
||||
args.emplace_back("webm");
|
||||
break;
|
||||
|
||||
default:
|
||||
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(*_outputParams.format)) + ")" };
|
||||
}
|
||||
}
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
|
||||
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||
for (const std::string& arg : args)
|
||||
LOG(DEBUG, "Arg = '" << arg << "'");
|
||||
|
||||
// Caution: stdin must have been closed before
|
||||
try
|
||||
{
|
||||
_childProcess = core::Service<core::IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
|
||||
}
|
||||
catch (core::ChildProcessException& exception)
|
||||
{
|
||||
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
|
||||
}
|
||||
}
|
||||
|
||||
void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
|
||||
{
|
||||
assert(_childProcess);
|
||||
|
||||
return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](core::IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) {
|
||||
readCallback(nbBytesRead);
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
|
||||
{
|
||||
assert(_childProcess);
|
||||
|
||||
return _childProcess->readSome(buffer, bufferSize);
|
||||
}
|
||||
|
||||
std::string_view Transcoder::getOutputMimeType() const
|
||||
{
|
||||
// TODO: use input mime type
|
||||
if (_outputParams.format)
|
||||
{
|
||||
switch (*_outputParams.format)
|
||||
{
|
||||
case OutputFormat::MP3:
|
||||
return "audio/mpeg";
|
||||
case OutputFormat::OGG_OPUS:
|
||||
return "audio/opus";
|
||||
case OutputFormat::MATROSKA_OPUS:
|
||||
return "audio/x-matroska";
|
||||
case OutputFormat::OGG_VORBIS:
|
||||
return "audio/ogg";
|
||||
case OutputFormat::WEBM_VORBIS:
|
||||
return "audio/webm";
|
||||
}
|
||||
}
|
||||
|
||||
return "application/octet-stream"; // default, should not happen
|
||||
}
|
||||
|
||||
bool Transcoder::finished() const
|
||||
{
|
||||
assert(_childProcess);
|
||||
|
||||
return _childProcess->finished();
|
||||
}
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/ITranscoder.hpp"
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
class IChildProcess;
|
||||
}
|
||||
|
||||
namespace lms::audio::ffmpeg
|
||||
{
|
||||
class Transcoder : public ITranscoder
|
||||
{
|
||||
public:
|
||||
Transcoder(const TranscodeParameters& parameters);
|
||||
~Transcoder() override;
|
||||
Transcoder(const Transcoder&) = delete;
|
||||
Transcoder& operator=(const Transcoder&) = delete;
|
||||
|
||||
private:
|
||||
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback) override;
|
||||
std::size_t readSome(std::byte* buffer, std::size_t bufferSize) override;
|
||||
|
||||
std::string_view getOutputMimeType() const override;
|
||||
const TranscodeOutputParameters& getOutputParameters() const override { return _outputParams; }
|
||||
|
||||
bool finished() const override;
|
||||
static void init();
|
||||
void start();
|
||||
|
||||
const std::size_t _debugId{};
|
||||
const TranscodeInputParameters _inputParams;
|
||||
const TranscodeOutputParameters _outputParams;
|
||||
std::unique_ptr<core::IChildProcess> _childProcess;
|
||||
};
|
||||
} // namespace lms::audio::ffmpeg
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::audio::ffmpeg::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
// TODO: list demuxers to retrieve supported formats
|
||||
static const std::array<std::filesystem::path, 18> fileExtensions{
|
||||
".aac",
|
||||
".alac",
|
||||
".aif",
|
||||
".aiff",
|
||||
".ape",
|
||||
".dsf",
|
||||
".flac",
|
||||
".m4a",
|
||||
".m4b",
|
||||
".mp3",
|
||||
".mpc",
|
||||
".oga",
|
||||
".ogg",
|
||||
".opus",
|
||||
".shn",
|
||||
".wav",
|
||||
".wma",
|
||||
".wv",
|
||||
};
|
||||
return fileExtensions;
|
||||
}
|
||||
} // namespace lms::audio::ffmpeg::utils
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
|
||||
namespace lms::audio::ffmpeg::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
} // namespace lms::audio::ffmpeg::utils
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AudioFileInfo.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
#if LMS_TAGLIB_HAS_SHORTEN
|
||||
#include <taglib/shortenfile.h>
|
||||
#endif
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
AudioProperties computeAudioProperties(const ::TagLib::File& file)
|
||||
{
|
||||
assert(file.audioProperties());
|
||||
const ::TagLib::AudioProperties& properties{ *file.audioProperties() };
|
||||
|
||||
AudioProperties audioProperties;
|
||||
|
||||
// Common properties
|
||||
audioProperties.bitrate = static_cast<std::size_t>(properties.bitrate() * 1000);
|
||||
audioProperties.channelCount = static_cast<std::size_t>(properties.channels());
|
||||
audioProperties.duration = std::chrono::milliseconds{ properties.lengthInMilliseconds() };
|
||||
audioProperties.sampleRate = static_cast<std::size_t>(properties.sampleRate());
|
||||
|
||||
// Guess container from the file type
|
||||
if (const auto* apeFile{ dynamic_cast<const ::TagLib::APE::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::APE;
|
||||
audioProperties.codec = CodecType::APE; // TODO version?
|
||||
audioProperties.bitsPerSample = apeFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::ASF;
|
||||
|
||||
switch (asfFile->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::ASF::Properties::Codec::WMA1:
|
||||
audioProperties.codec = CodecType::WMA1;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA2:
|
||||
audioProperties.codec = CodecType::WMA2;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Lossless:
|
||||
audioProperties.codec = CodecType::WMA9Lossless;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::WMA9Pro:
|
||||
audioProperties.codec = CodecType::WMA9Pro;
|
||||
break;
|
||||
case ::TagLib::ASF::Properties::Codec::Unknown:
|
||||
audioProperties.codec = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = asfFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* dsfFile{ dynamic_cast<const ::TagLib::DSF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::DSF;
|
||||
audioProperties.codec = CodecType::DSD;
|
||||
audioProperties.bitsPerSample = dsfFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_DSF
|
||||
else if (const auto* flacFile{ dynamic_cast<const ::TagLib::FLAC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::FLAC;
|
||||
audioProperties.codec = CodecType::FLAC;
|
||||
audioProperties.bitsPerSample = flacFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MP4;
|
||||
switch (mp4File->audioProperties()->codec())
|
||||
{
|
||||
case ::TagLib::MP4::Properties::Codec::AAC:
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::ALAC:
|
||||
audioProperties.codec = CodecType::ALAC;
|
||||
break;
|
||||
case ::TagLib::MP4::Properties::Codec::Unknown:
|
||||
audioProperties.codec = std::nullopt;
|
||||
break;
|
||||
}
|
||||
|
||||
audioProperties.bitsPerSample = mp4File->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* mpcFile{ dynamic_cast<const ::TagLib::MPC::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::MPC;
|
||||
|
||||
switch (mpcFile->audioProperties()->mpcVersion())
|
||||
{
|
||||
case 7:
|
||||
audioProperties.codec = CodecType::MPC7;
|
||||
break;
|
||||
case 8:
|
||||
audioProperties.codec = CodecType::MPC8;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (const auto* mpegFile{ dynamic_cast<const ::TagLib::MPEG::File*>(&file) })
|
||||
{
|
||||
const auto& properties{ *mpegFile->audioProperties() };
|
||||
|
||||
audioProperties.container = ContainerType::MPEG;
|
||||
if ((properties.version() == TagLib::MPEG::Header::Version::Version1 || properties.version() == TagLib::MPEG::Header::Version::Version2 || properties.version() == TagLib::MPEG::Header::Version::Version2_5)
|
||||
&& mpegFile->audioProperties()->layer() == 3)
|
||||
audioProperties.codec = CodecType::MP3; // could be MPEG-1 layer 3 or MPEG-2(.5) layer 3
|
||||
else if (mpegFile->audioProperties()->isADTS()) // likely AAC
|
||||
audioProperties.codec = CodecType::AAC;
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Opus::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Opus;
|
||||
}
|
||||
else if (dynamic_cast<const ::TagLib::Ogg::Vorbis::File*>(&file))
|
||||
{
|
||||
audioProperties.container = ContainerType::Ogg;
|
||||
audioProperties.codec = CodecType::Vorbis;
|
||||
}
|
||||
else if (const auto* aiffFile{ dynamic_cast<const ::TagLib::RIFF::AIFF::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::AIFF;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.bitsPerSample = aiffFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavFile{ dynamic_cast<const ::TagLib::RIFF::WAV::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WAV;
|
||||
audioProperties.codec = CodecType::PCM;
|
||||
audioProperties.bitsPerSample = wavFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#if LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* shortenFile{ dynamic_cast<const ::TagLib::Shorten::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::Shorten;
|
||||
audioProperties.codec = CodecType::Shorten;
|
||||
audioProperties.bitsPerSample = shortenFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_SHORTEN
|
||||
else if (const auto* trueAudioFile{ dynamic_cast<const ::TagLib::TrueAudio::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::TrueAudio;
|
||||
audioProperties.codec = CodecType::TrueAudio;
|
||||
audioProperties.bitsPerSample = trueAudioFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
else if (const auto* wavPackFile{ dynamic_cast<const ::TagLib::WavPack::File*>(&file) })
|
||||
{
|
||||
audioProperties.container = ContainerType::WavPack;
|
||||
audioProperties.codec = CodecType::WavPack;
|
||||
audioProperties.bitsPerSample = wavPackFile->audioProperties()->bitsPerSample();
|
||||
}
|
||||
|
||||
return audioProperties;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AudioFileInfo::AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs)
|
||||
: _filePath{ filePath }
|
||||
, _file{ utils::parseFile(filePath, readStyle) }
|
||||
, _audioProperties{ computeAudioProperties(*_file) }
|
||||
, _tagReader{ *_file, enableExtraDebugLogs }
|
||||
, _imageReader{ *_file }
|
||||
{
|
||||
}
|
||||
|
||||
const AudioProperties& AudioFileInfo::getAudioProperties() const
|
||||
{
|
||||
return _audioProperties;
|
||||
}
|
||||
|
||||
const IImageReader& AudioFileInfo::getImageReader() const
|
||||
{
|
||||
return _imageReader;
|
||||
}
|
||||
|
||||
const ITagReader& AudioFileInfo::getTagReader() const
|
||||
{
|
||||
return _tagReader;
|
||||
}
|
||||
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "audio/AudioTypes.hpp"
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
#include "audio/IImageReader.hpp"
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
#include "ImageReader.hpp"
|
||||
#include "TagReader.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class AudioFileInfo final : public IAudioFileInfo
|
||||
{
|
||||
public:
|
||||
AudioFileInfo(const std::filesystem::path& filePath, ParserOptions::AudioPropertiesReadStyle readStyle, bool enableExtraDebugLogs);
|
||||
|
||||
private:
|
||||
const AudioProperties& getAudioProperties() const override;
|
||||
const IImageReader& getImageReader() const override;
|
||||
const ITagReader& getTagReader() const override;
|
||||
|
||||
const std::filesystem::path _filePath;
|
||||
std::unique_ptr<::TagLib::File> _file;
|
||||
const AudioProperties _audioProperties;
|
||||
const TagReader _tagReader;
|
||||
const ImageReader _imageReader;
|
||||
};
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,418 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ImageReader.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/attachedpictureframe.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/flacpicture.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/mp4coverart.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Image::Type imageTypeFromfromID3v2(TagLib::ID3v2::AttachedPictureFrame::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case ::TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
}
|
||||
|
||||
Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::ASF::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case ::TagLib::ASF::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case ::TagLib::ASF::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case ::TagLib::ASF::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case ::TagLib::ASF::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case ::TagLib::ASF::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case ::TagLib::ASF::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case ::TagLib::ASF::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case ::TagLib::ASF::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case ::TagLib::ASF::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case ::TagLib::ASF::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case ::TagLib::ASF::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case ::TagLib::ASF::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case ::TagLib::ASF::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case ::TagLib::ASF::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case ::TagLib::ASF::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case ::TagLib::ASF::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case ::TagLib::ASF::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case ::TagLib::ASF::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case ::TagLib::ASF::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case ::TagLib::ASF::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
}
|
||||
|
||||
Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::TagLib::FLAC::Picture::Type::Other:
|
||||
return Image::Type::Other;
|
||||
case ::TagLib::FLAC::Picture::Type::FileIcon:
|
||||
return Image::Type::FileIcon;
|
||||
case ::TagLib::FLAC::Picture::Type::OtherFileIcon:
|
||||
return Image::Type::OtherFileIcon;
|
||||
case ::TagLib::FLAC::Picture::Type::FrontCover:
|
||||
return Image::Type::FrontCover;
|
||||
case ::TagLib::FLAC::Picture::Type::BackCover:
|
||||
return Image::Type::BackCover;
|
||||
case ::TagLib::FLAC::Picture::Type::LeafletPage:
|
||||
return Image::Type::LeafletPage;
|
||||
case ::TagLib::FLAC::Picture::Type::Media:
|
||||
return Image::Type::Media;
|
||||
case ::TagLib::FLAC::Picture::Type::LeadArtist:
|
||||
return Image::Type::LeadArtist;
|
||||
case ::TagLib::FLAC::Picture::Type::Artist:
|
||||
return Image::Type::Artist;
|
||||
case ::TagLib::FLAC::Picture::Type::Conductor:
|
||||
return Image::Type::Conductor;
|
||||
case ::TagLib::FLAC::Picture::Type::Band:
|
||||
return Image::Type::Band;
|
||||
case ::TagLib::FLAC::Picture::Type::Composer:
|
||||
return Image::Type::Composer;
|
||||
case ::TagLib::FLAC::Picture::Type::Lyricist:
|
||||
return Image::Type::Lyricist;
|
||||
case ::TagLib::FLAC::Picture::Type::RecordingLocation:
|
||||
return Image::Type::RecordingLocation;
|
||||
case ::TagLib::FLAC::Picture::Type::DuringRecording:
|
||||
return Image::Type::DuringRecording;
|
||||
case ::TagLib::FLAC::Picture::Type::DuringPerformance:
|
||||
return Image::Type::DuringPerformance;
|
||||
case ::TagLib::FLAC::Picture::Type::MovieScreenCapture:
|
||||
return Image::Type::MovieScreenCapture;
|
||||
case ::TagLib::FLAC::Picture::Type::ColouredFish:
|
||||
return Image::Type::ColouredFish;
|
||||
case ::TagLib::FLAC::Picture::Type::Illustration:
|
||||
return Image::Type::Illustration;
|
||||
case ::TagLib::FLAC::Picture::Type::BandLogo:
|
||||
return Image::Type::BandLogo;
|
||||
case ::TagLib::FLAC::Picture::Type::PublisherLogo:
|
||||
return Image::Type::PublisherLogo;
|
||||
}
|
||||
|
||||
return Image::Type::Unknown;
|
||||
}
|
||||
|
||||
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ::TagLib::MP4::CoverArt::Format::BMP:
|
||||
return "image/bmp";
|
||||
case ::TagLib::MP4::CoverArt::Format::GIF:
|
||||
return "image/gif";
|
||||
case ::TagLib::MP4::CoverArt::Format::JPEG:
|
||||
return "image/jpeg";
|
||||
case ::TagLib::MP4::CoverArt::Format::PNG:
|
||||
return "image/png";
|
||||
case ::TagLib::MP4::CoverArt::Format::Unknown:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
|
||||
{
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
|
||||
return Image::Type::FrontCover;
|
||||
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
|
||||
return Image::Type::BackCover;
|
||||
|
||||
return Image::Type::Unknown;
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
|
||||
void visitID3V2Images(const ::TagLib::ID3v2::Tag& id3v2Tags, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
const auto& frameListMap{ id3v2Tags.frameListMap() };
|
||||
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
|
||||
{
|
||||
const auto* attachedPictureFrame{ dynamic_cast<const ::TagLib::ID3v2::AttachedPictureFrame*>(frame) };
|
||||
if (!attachedPictureFrame)
|
||||
continue;
|
||||
|
||||
::TagLib::ByteVector picture{ attachedPictureFrame->picture() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
image.type = imageTypeFromfromID3v2(attachedPictureFrame->type());
|
||||
image.description = attachedPictureFrame->description().to8Bit(true);
|
||||
image.mimeType = attachedPictureFrame->mimeType().to8Bit(true);
|
||||
image.data = pictureData;
|
||||
visitor(image);
|
||||
}
|
||||
}
|
||||
|
||||
void visitASFImages(const ::TagLib::ASF::Tag& asfTags, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
for (const ::TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
|
||||
{
|
||||
::TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
|
||||
if (!asfPicture.isValid())
|
||||
continue;
|
||||
|
||||
::TagLib::ByteVector picture{ asfPicture.picture() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
image.type = imageTypeFromfromASF(asfPicture.type());
|
||||
image.description = asfPicture.description().to8Bit(true);
|
||||
image.mimeType = asfPicture.mimeType().to8Bit(true);
|
||||
image.data = pictureData;
|
||||
|
||||
visitor(image);
|
||||
}
|
||||
}
|
||||
|
||||
void visitMP4Images(const ::TagLib::MP4::File& mp4File, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
const ::TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
|
||||
if (!coverItem.isValid())
|
||||
return;
|
||||
|
||||
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (coverItem.type() != ::TagLib::MP4::Item::Type::CoverArtList)
|
||||
return;
|
||||
#endif
|
||||
::TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
|
||||
|
||||
bool firstCover{ true };
|
||||
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
|
||||
{
|
||||
::TagLib::ByteVector picture{ coverArt.data() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
image.mimeType = mp4ImageFormatToMimeType(coverArt.format());
|
||||
image.data = pictureData;
|
||||
|
||||
// By convention, consider the first cover art as the front cover
|
||||
image.type = firstCover ? Image::Type::FrontCover : Image::Type::Unknown;
|
||||
firstCover = false;
|
||||
|
||||
visitor(image);
|
||||
}
|
||||
}
|
||||
|
||||
void visitFLACImages(const ::TagLib::List<TagLib::FLAC::Picture*>& pictureList, const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
for (TagLib::FLAC::Picture* flacPicture : pictureList)
|
||||
{
|
||||
::TagLib::ByteVector picture{ flacPicture->data() };
|
||||
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
|
||||
Image image;
|
||||
image.type = imageTypeFromfromFLAC(flacPicture->type());
|
||||
image.description = flacPicture->description().to8Bit(true);
|
||||
image.mimeType = flacPicture->mimeType().to8Bit(true);
|
||||
image.data = pictureData;
|
||||
|
||||
visitor(image);
|
||||
}
|
||||
}
|
||||
|
||||
void visitAPEImages([[maybe_unused]] const ::TagLib::APE::Tag& apeTags, [[maybe_unused]] const ImageReader::ImageVisitor& visitor)
|
||||
{
|
||||
#if LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
const ::TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
|
||||
for (const ::TagLib::VariantMap& pictureProperty : pictureProperties)
|
||||
{
|
||||
Image image;
|
||||
::TagLib::ByteVector picture;
|
||||
|
||||
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
|
||||
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
|
||||
if (auto it{ pictureProperty.find("mimeType") }; it != pictureProperty.cend())
|
||||
image.mimeType = it->second.toString().to8Bit(true);
|
||||
if (auto it{ pictureProperty.find("description") }; it != pictureProperty.cend())
|
||||
image.description = it->second.toString().to8Bit(true);
|
||||
if (auto it{ pictureProperty.find("data") }; it != pictureProperty.cend())
|
||||
{
|
||||
picture = it->second.toByteVector();
|
||||
image.data = { reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
|
||||
}
|
||||
|
||||
if (!image.data.empty())
|
||||
visitor(image);
|
||||
}
|
||||
#endif // LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ImageReader::ImageReader(::TagLib::File& file)
|
||||
: _file{ file }
|
||||
{
|
||||
}
|
||||
|
||||
ImageReader::~ImageReader() = default;
|
||||
|
||||
void ImageReader::visitImages(const ImageVisitor& visitor) const
|
||||
{
|
||||
// MP3
|
||||
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
visitID3V2Images(*mp3File->ID3v2Tag(), visitor);
|
||||
}
|
||||
// MP4
|
||||
else if (const TagLib::MP4::File * mp4File{ dynamic_cast<const TagLib::MP4::File*>(&_file) })
|
||||
{
|
||||
visitMP4Images(*mp4File, visitor);
|
||||
}
|
||||
// WMA
|
||||
else if (const TagLib::ASF::File * asfFile{ dynamic_cast<const TagLib::ASF::File*>(&_file) })
|
||||
{
|
||||
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
visitASFImages(*tag, visitor);
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // usage discouraged
|
||||
visitID3V2Images(*flacFile->ID3v2Tag(), visitor);
|
||||
else
|
||||
visitFLACImages(flacFile->pictureList(), visitor);
|
||||
}
|
||||
// Ogg vorbis
|
||||
else if (const TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<const TagLib::Ogg::Vorbis::File*>(&_file) })
|
||||
{
|
||||
visitFLACImages(vorbisFile->tag()->pictureList(), visitor);
|
||||
}
|
||||
// Ogg Opus
|
||||
else if (const TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(&_file) })
|
||||
{
|
||||
visitFLACImages(opusFile->tag()->pictureList(), visitor);
|
||||
}
|
||||
// Aiff
|
||||
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(&_file) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
visitID3V2Images(*aiffFile->tag(), visitor);
|
||||
}
|
||||
// Wav
|
||||
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(&_file) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
visitID3V2Images(*wavFile->ID3v2Tag(), visitor);
|
||||
}
|
||||
// MPC
|
||||
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
|
||||
{
|
||||
if (mpcFile->hasAPETag())
|
||||
visitAPEImages(*mpcFile->APETag(), visitor);
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
|
||||
{
|
||||
if (wavPackFile->hasAPETag())
|
||||
visitAPEImages(*wavPackFile->APETag(), visitor);
|
||||
}
|
||||
}
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/IImageReader.hpp"
|
||||
|
||||
namespace TagLib
|
||||
{
|
||||
class File;
|
||||
}
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class ImageReader : public IImageReader
|
||||
{
|
||||
public:
|
||||
ImageReader(TagLib::File& _file);
|
||||
~ImageReader() override;
|
||||
ImageReader(const ImageReader&) = delete;
|
||||
ImageReader& operator=(const ImageReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitImages(const ImageVisitor& visitor) const override;
|
||||
|
||||
TagLib::File& _file;
|
||||
};
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <taglib/taglib.h>
|
||||
|
||||
#if (TAGLIB_MAJOR_VERSION >= 2)
|
||||
#define LMS_TAGLIB_HAS_DSF 1
|
||||
#endif
|
||||
|
||||
// LMS_TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
|
||||
#define LMS_TAGLIB_HAS_MP4_ITEM_TYPE 1
|
||||
#endif
|
||||
|
||||
// LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
|
||||
#define LMS_TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
|
||||
#endif
|
||||
|
||||
// LMS_TAGLIB_HAS_SHORTEN if version >= 2.1
|
||||
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION >= 1))
|
||||
#define LMS_TAGLIB_HAS_SHORTEN 1
|
||||
#endif
|
||||
@@ -0,0 +1,482 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TagReader.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/oggflacfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/speexfile.h>
|
||||
#include <taglib/synchronizedlyricsframe.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/unsynchronizedlyricsframe.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsdifffile.h>
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Mapping to internal taglib names and/or common alternative custom names
|
||||
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
|
||||
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||
{ TagType::Album, { "ALBUM" } },
|
||||
{ TagType::AlbumArtist, { "ALBUMARTIST" } },
|
||||
{ TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT" } },
|
||||
{ TagType::AlbumArtists, { "ALBUMARTISTS" } },
|
||||
{ TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
|
||||
{ TagType::AlbumComment, { "ALBUMCOMMENT", "MUSICBRAINZ_ALBUMCOMMENT, MUSICBRAINZ ALBUM COMMENT", "ALBUMVERSION", "VERSION" } },
|
||||
{ TagType::AlbumSortOrder, { "ALBUMSORT" } },
|
||||
{ TagType::Arranger, { "ARRANGER" } },
|
||||
{ TagType::Artist, { "ARTIST" } },
|
||||
{ TagType::ArtistSortOrder, { "ARTISTSORT" } },
|
||||
{ TagType::Artists, { "ARTISTS" } },
|
||||
{ TagType::ArtistsSortOrder, { "ARTISTSSORT" } },
|
||||
{ TagType::ASIN, { "ASIN" } },
|
||||
{ TagType::Barcode, { "BARCODE" } },
|
||||
{ TagType::BPM, { "BPM" } },
|
||||
{ TagType::CatalogNumber, { "CATALOGNUMBER" } },
|
||||
{ TagType::Comment, { "COMMENT" } },
|
||||
{ TagType::Compilation, { "COMPILATION" } },
|
||||
{ TagType::Composer, { "COMPOSER" } },
|
||||
{ TagType::Composers, { "COMPOSERS" } },
|
||||
{ TagType::ComposerSortOrder, { "COMPOSERSORT" } },
|
||||
{ TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
|
||||
{ TagType::Conductor, { "CONDUCTOR" } },
|
||||
{ TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
|
||||
{ TagType::Conductors, { "CONDUCTORS" } },
|
||||
{ TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
|
||||
{ TagType::Copyright, { "COPYRIGHT" } },
|
||||
{ TagType::CopyrightURL, { "COPYRIGHTURL" } },
|
||||
{ TagType::Date, { "DATE", "YEAR" } },
|
||||
{ TagType::Director, { "DIRECTOR" } },
|
||||
{ TagType::DiscNumber, { "DISCNUMBER", "DISC" } },
|
||||
{ TagType::DiscSubtitle, { "DISCSUBTITLE", "SETSUBTITLE" } },
|
||||
{ TagType::EncodedBy, { "ENCODEDBY" } },
|
||||
{ TagType::Engineer, { "ENGINEER" } },
|
||||
{ TagType::EncodingTime, { "ENCODINGTIME" } },
|
||||
{ TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
|
||||
{ TagType::Genre, { "GENRE" } },
|
||||
{ TagType::Grouping, { "GROUPING", "ALBUMGROUPING" } },
|
||||
{ TagType::InitialKey, { "INITIALKEY" } },
|
||||
{ TagType::ISRC, { "ISRC" } },
|
||||
{ TagType::Language, { "LANGUAGE" } },
|
||||
{ TagType::License, { "LICENSE" } },
|
||||
{ TagType::Lyricist, { "LYRICIST" } },
|
||||
{ TagType::LyricistSortOrder, { "LYRICISTSORT" } },
|
||||
{ TagType::Lyricists, { "LYRICISTS" } },
|
||||
{ TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
|
||||
{ TagType::Media, { "MEDIA" } },
|
||||
{ TagType::MixDJ, { "DJMIXER" } },
|
||||
{ TagType::Mixer, { "MIXER" } },
|
||||
{ TagType::MixerSortOrder, { "MIXERSORT" } },
|
||||
{ TagType::Mixers, { "MIXERS" } },
|
||||
{ TagType::MixersSortOrder, { "MIXERSSORT" } },
|
||||
{ TagType::Mood, { "MOOD" } },
|
||||
{ TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
|
||||
{ TagType::MovementCount, { "MOVEMENTCOUNT" } },
|
||||
{ TagType::MovementNumber, { "MOVEMENTNUMBER" } },
|
||||
{ TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
|
||||
{ TagType::MusicBrainzArrangerID, { "MUSICBRAINZ_ARRANGERID", "MUSICBRAINZ ARRANGER ID", "MUSICBRAINZ/ARRANGER ID" } },
|
||||
{ TagType::MusicBrainzComposerID, { "MUSICBRAINZ_COMPOSERID", "MUSICBRAINZ COMPOSER ID", "MUSICBRAINZ/COMPOSER ID" } },
|
||||
{ TagType::MusicBrainzConductorID, { "MUSICBRAINZ_CONDUCTORID", "MUSICBRAINZ CONDUCTOR ID", "MUSICBRAINZ/CONDUCTOR ID" } },
|
||||
{ TagType::MusicBrainzDirectorID, { "MUSICBRAINZ_DIRECTORID", "MUSICBRAINZ DIRECTOR ID", "MUSICBRAINZ/DIRECTOR ID" } },
|
||||
{ TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
|
||||
{ TagType::MusicBrainzLyricistID, { "MUSICBRAINZ_LYRICISTID", "MUSICBRAINZ LYRICIST ID", "MUSICBRAINZ/LYRICIST ID" } },
|
||||
{ TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
|
||||
{ TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
|
||||
{ TagType::MusicBrainzMixerID, { "MUSICBRAINZ_MIXERID", "MUSICBRAINZ MIXER ID", "MUSICBRAINZ/MIXER ID" } },
|
||||
{ TagType::MusicBrainzProducerID, { "MUSICBRAINZ_PRODUCERID", "MUSICBRAINZ PRODUCER ID", "MUSICBRAINZ/PRODUCER ID" } },
|
||||
{ TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
|
||||
{ TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
|
||||
{ TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
|
||||
{ TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
|
||||
{ TagType::MusicBrainzRemixerID, { "MUSICBRAINZ_REMIXERID", "MUSICBRAINZ REMIXER ID", "MUSICBRAINZ/REMIXER ID" } },
|
||||
{ TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
|
||||
{ TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
|
||||
{ TagType::OriginalArtist, { "ORIGINALARTIST" } },
|
||||
{ TagType::OriginalFilename, { "ORIGINALFILENAME" } },
|
||||
{ TagType::OriginalReleaseDate, { "ORIGINALDATE" } },
|
||||
{ TagType::OriginalReleaseYear, { "ORIGINALYEAR" } },
|
||||
{ TagType::Podcast, { "PODCAST" } },
|
||||
{ TagType::PodcastURL, { "PODCASTURL" } },
|
||||
{ TagType::Producer, { "PRODUCER" } },
|
||||
{ TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
|
||||
{ TagType::Producers, { "PRODUCERS" } },
|
||||
{ TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
|
||||
{ TagType::RecordLabel, { "LABEL", "PUBLISHER", "ORGANIZATION" } },
|
||||
{ TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
|
||||
{ TagType::ReleaseDate, { "RELEASEDATE" } },
|
||||
{ TagType::ReleaseStatus, { "RELEASESTATUS" } },
|
||||
{ TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
|
||||
{ TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
|
||||
{ TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
|
||||
{ TagType::Remixers, { "REMIXERS" } },
|
||||
{ TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
|
||||
{ TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
|
||||
{ TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
|
||||
{ TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
|
||||
{ TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
|
||||
{ TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
|
||||
{ TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
|
||||
{ TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
|
||||
{ TagType::Script, { "SCRIPT" } },
|
||||
{ TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
|
||||
{ TagType::Subtitle, { "SUBTITLE" } },
|
||||
{ TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS" } },
|
||||
{ TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
|
||||
{ TagType::TrackNumber, { "TRACKNUMBER" } },
|
||||
{ TagType::TrackTitle, { "TITLE" } },
|
||||
{ TagType::TrackTitleSortOrder, { "TITLESORT" } },
|
||||
{ TagType::WorkTitle, { "WORK" } },
|
||||
{ TagType::Writer, { "WRITER" } },
|
||||
};
|
||||
|
||||
void mergeTagMaps(TagLib::PropertyMap& dst, ::TagLib::PropertyMap&& src)
|
||||
{
|
||||
for (auto&& [tag, values] : src)
|
||||
{
|
||||
if (dst.find(tag) == std::cend(dst))
|
||||
dst[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
|
||||
void dedupTagValues(TagLib::PropertyMap& propertyMap)
|
||||
{
|
||||
for (auto& [key, values] : propertyMap)
|
||||
{
|
||||
if (values.size() <= 1)
|
||||
continue;
|
||||
|
||||
::TagLib::StringList newList;
|
||||
for (const ::TagLib::String& value : values)
|
||||
{
|
||||
if (!std::any_of(std::cbegin(newList), std::cend(newList), [&](const ::TagLib::String& v) { return v == value; }))
|
||||
newList.append(value);
|
||||
}
|
||||
|
||||
if (values != newList)
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "Removed " << (values.size() - newList.size()) << " duplicated value(s) in tag '" << key << "', " << newList.size() << " remaining value(s)");
|
||||
values = newList;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TagReader::TagReader(::TagLib::File& file, bool enableExtraDebugLogs)
|
||||
: _file{ file }
|
||||
{
|
||||
_propertyMap = _file.properties();
|
||||
|
||||
enableExtraDebugLogs &= core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG);
|
||||
if (enableExtraDebugLogs)
|
||||
{
|
||||
for (const auto& [key, values] : _propertyMap)
|
||||
{
|
||||
for (const auto& value : values)
|
||||
LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value.to8Bit(true) << "'");
|
||||
}
|
||||
|
||||
for (const auto& value : _propertyMap.unsupportedData())
|
||||
LMS_LOG(METADATA, DEBUG, "Unknown value: '" << value.to8Bit(true) << "'");
|
||||
}
|
||||
|
||||
// Some tags may not be known by TagLib
|
||||
auto getAPETags = [&](const ::TagLib::APE::Tag* apeTag) {
|
||||
if (!apeTag)
|
||||
return;
|
||||
|
||||
mergeTagMaps(_propertyMap, apeTag->properties());
|
||||
};
|
||||
|
||||
auto processID3v2Tags = [&](TagLib::ID3v2::Tag& id3v2Tags) {
|
||||
// Dedup values for some tags that may be written in both a standard tag and in a custom tag
|
||||
dedupTagValues(_propertyMap);
|
||||
|
||||
const auto& frameListMap{ id3v2Tags.frameListMap() };
|
||||
|
||||
// Get some extra tags that may not be known by taglib
|
||||
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
|
||||
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
|
||||
|
||||
// consider each frame hold a different set of lyrics
|
||||
// Synchronized lyrics frames
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["SYLT"])
|
||||
{
|
||||
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::SynchronizedLyricsFrame*>(frame) };
|
||||
if (!lyricsFrame)
|
||||
continue; // TODO log or assert?
|
||||
|
||||
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
|
||||
std::string lyrics;
|
||||
for (const ::TagLib::ID3v2::SynchronizedLyricsFrame::SynchedText& synchedText : lyricsFrame->synchedText())
|
||||
{
|
||||
std::chrono::milliseconds timestamp{};
|
||||
switch (lyricsFrame->timestampFormat())
|
||||
{
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds:
|
||||
timestamp = std::chrono::milliseconds{ synchedText.time };
|
||||
break;
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::AbsoluteMpegFrames:
|
||||
{
|
||||
const ::TagLib::AudioProperties* properties{ file.audioProperties() };
|
||||
if (properties && properties->sampleRate())
|
||||
timestamp = std::chrono::milliseconds{ synchedText.time * 1000 / properties->sampleRate() };
|
||||
}
|
||||
break;
|
||||
case ::TagLib::ID3v2::SynchronizedLyricsFrame::Unknown:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!lyrics.empty())
|
||||
lyrics += '\n';
|
||||
|
||||
lyrics += core::stringUtils::formatTimestamp(timestamp);
|
||||
lyrics += synchedText.text.to8Bit(true);
|
||||
}
|
||||
|
||||
_id3v2Lyrics.emplace(language, std::move(lyrics));
|
||||
}
|
||||
|
||||
// Unsynchronized lyrics frames
|
||||
for (const ::TagLib::ID3v2::Frame* frame : frameListMap["USLT"])
|
||||
{
|
||||
const auto* lyricsFrame{ dynamic_cast<const ::TagLib::ID3v2::UnsynchronizedLyricsFrame*>(frame) };
|
||||
if (!lyricsFrame)
|
||||
continue; // TODO log or assert?
|
||||
|
||||
const std::string language{ lyricsFrame->language().data(), lyricsFrame->language().size() };
|
||||
_id3v2Lyrics.emplace(language, lyricsFrame->text().to8Bit(true));
|
||||
}
|
||||
};
|
||||
|
||||
// WMA
|
||||
if (const ::TagLib::ASF::File * asfFile{ dynamic_cast<const ::TagLib::ASF::File*>(&_file) })
|
||||
{
|
||||
if (const ::TagLib::ASF::Tag * tag{ asfFile->tag() })
|
||||
{
|
||||
for (const auto& [name, attributeList] : tag->attributeListMap())
|
||||
{
|
||||
if (attributeList.isEmpty())
|
||||
continue;
|
||||
|
||||
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
|
||||
if (enableExtraDebugLogs)
|
||||
{
|
||||
for (const auto& attribute : attributeList)
|
||||
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : ::TagLib::String{ "<Non unicode>" }) << "'");
|
||||
}
|
||||
|
||||
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
|
||||
continue;
|
||||
|
||||
::TagLib::StringList strAttributes;
|
||||
for (const ::TagLib::ASF::Attribute& attribute : attributeList)
|
||||
{
|
||||
if (attribute.type() == ::TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
|
||||
strAttributes.append(attribute.toString());
|
||||
}
|
||||
|
||||
if (!strAttributes.isEmpty())
|
||||
_propertyMap[strName] = strAttributes;
|
||||
}
|
||||
|
||||
// Merge artists that may have been saved only in Author (see #597)
|
||||
if (auto itAuthor{ _propertyMap.find("AUTHOR") }; itAuthor != _propertyMap.end() && _propertyMap.unsupportedData().contains("Author"))
|
||||
{
|
||||
if (!_propertyMap.contains("ARTISTS"))
|
||||
{
|
||||
auto& artistEntries{ _propertyMap["ARTIST"] };
|
||||
for (const auto& author : itAuthor->second)
|
||||
{
|
||||
if (!artistEntries.contains(author))
|
||||
artistEntries.append(author);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// MP3
|
||||
else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(&_file) })
|
||||
{
|
||||
if (mp3File->hasID3v2Tag())
|
||||
processID3v2Tags(*mp3File->ID3v2Tag(false));
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
// MP4
|
||||
else if (const ::TagLib::MP4::File * mp4File{ dynamic_cast<const ::TagLib::MP4::File*>(&_file) })
|
||||
{
|
||||
// Taglib does not expose rtng in properties
|
||||
if (const ::TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
|
||||
{
|
||||
#if LMS_TAGLIB_HAS_MP4_ITEM_TYPE
|
||||
if (rtngItem.type() == ::TagLib::MP4::Item::Type::Byte)
|
||||
#endif
|
||||
_propertyMap["ITUNESADVISORY"] = ::TagLib::String{ std::to_string(rtngItem.toByte()) };
|
||||
}
|
||||
|
||||
if (!_propertyMap.contains("ORIGINALDATE"))
|
||||
{
|
||||
// For now:
|
||||
// * TagLib 2.0 only parses ----:com.apple.iTunes:ORIGINALDATE
|
||||
// / TagLib <2.0 only parses ----:com.apple.iTunes:originaldate
|
||||
const auto& tags{ mp4File->tag()->itemMap() };
|
||||
for (const auto& origDateString : { "----:com.apple.iTunes:originaldate", "----:com.apple.iTunes:ORIGINALDATE" })
|
||||
{
|
||||
auto itOrigDateTag{ tags.find(origDateString) };
|
||||
if (itOrigDateTag != std::cend(tags))
|
||||
{
|
||||
const ::TagLib::StringList dates{ itOrigDateTag->second.toStringList() };
|
||||
if (!dates.isEmpty())
|
||||
{
|
||||
_propertyMap["ORIGINALDATE"] = dates.front();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// MPC
|
||||
else if (::TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(&_file) })
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(&_file) })
|
||||
{
|
||||
getAPETags(wavPackFile->APETag());
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(&_file) })
|
||||
{
|
||||
if (flacFile->hasID3v2Tag()) // discouraged usage
|
||||
processID3v2Tags(*flacFile->ID3v2Tag());
|
||||
}
|
||||
else if (const TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<const TagLib::RIFF::AIFF::File*>(&_file) })
|
||||
{
|
||||
if (aiffFile->hasID3v2Tag())
|
||||
processID3v2Tags(*aiffFile->tag());
|
||||
}
|
||||
else if (const TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<const TagLib::RIFF::WAV::File*>(&_file) })
|
||||
{
|
||||
if (wavFile->hasID3v2Tag())
|
||||
processID3v2Tags(*wavFile->ID3v2Tag());
|
||||
}
|
||||
}
|
||||
|
||||
TagReader::~TagReader() = default;
|
||||
|
||||
void TagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ tagLibTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(tagLibTagMapping))
|
||||
return;
|
||||
|
||||
for (const std::string& tagName : itTagNames->second)
|
||||
{
|
||||
bool visited{};
|
||||
|
||||
visitTagValues(tagName, [&](std::string_view value) {
|
||||
visited = true;
|
||||
visitor(value);
|
||||
});
|
||||
|
||||
if (visited)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TagReader::visitTagValues(std::string_view tag, TagValueVisitor visitor) const
|
||||
{
|
||||
::TagLib::String key{ tag.data() /* assume null terminated */, ::TagLib::String::Type::UTF8 };
|
||||
|
||||
auto itValues{ _propertyMap.find(key) };
|
||||
if (itValues == std::cend(_propertyMap))
|
||||
return;
|
||||
|
||||
for (const ::TagLib::String& value : itValues->second)
|
||||
visitor(value.to8Bit(true));
|
||||
}
|
||||
|
||||
void TagReader::visitPerformerTags(PerformerVisitor visitor) const
|
||||
{
|
||||
visitTagValues("PERFORMER", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
});
|
||||
|
||||
for (const auto& [key, values] : _propertyMap)
|
||||
{
|
||||
if (key.startsWith("PERFORMER:")) // startsWith is not case sensitive
|
||||
{
|
||||
std::string performerStr{ key.to8Bit(true) };
|
||||
const std::size_t rolePos{ performerStr.find(':') };
|
||||
assert(rolePos != std::string::npos);
|
||||
|
||||
std::string_view role{ std::string_view{ performerStr }.substr(rolePos + 1) };
|
||||
for (const ::TagLib::String& value : values)
|
||||
{
|
||||
const std::string name{ value.to8Bit(true) };
|
||||
visitor(role, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TagReader::visitLyricsTags(LyricsVisitor visitor) const
|
||||
{
|
||||
if (!_id3v2Lyrics.empty())
|
||||
{
|
||||
for (const auto& [language, lyrics] : _id3v2Lyrics)
|
||||
visitor(language, lyrics);
|
||||
}
|
||||
else
|
||||
{
|
||||
// otherwise, just visit regular LYRICS tag with no language
|
||||
visitTagValues("LYRICS", [&](std::string_view value) {
|
||||
visitor("", value);
|
||||
});
|
||||
}
|
||||
}
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <taglib/tpropertymap.h>
|
||||
|
||||
#include "audio/ITagReader.hpp"
|
||||
|
||||
namespace TagLib
|
||||
{
|
||||
class File;
|
||||
}
|
||||
|
||||
namespace lms::audio::taglib
|
||||
{
|
||||
class TagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
TagReader(::TagLib::File& file, bool enableExtraDebugLogs);
|
||||
~TagReader() override;
|
||||
|
||||
TagReader(const TagReader&) = delete;
|
||||
TagReader& operator=(const TagReader&) = delete;
|
||||
|
||||
private:
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
void visitLyricsTags(LyricsVisitor visitor) const override;
|
||||
|
||||
::TagLib::File& _file;
|
||||
::TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
|
||||
};
|
||||
} // namespace lms::audio::taglib
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include "TagLibDefs.hpp"
|
||||
|
||||
#include <taglib/aifffile.h>
|
||||
#include <taglib/apefile.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/audioproperties.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/id3v2framefactory.h>
|
||||
#include <taglib/mp4file.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/oggflacfile.h>
|
||||
#include <taglib/opusfile.h>
|
||||
#include <taglib/speexfile.h>
|
||||
#include <taglib/tfile.h>
|
||||
#include <taglib/tfilestream.h>
|
||||
#include <taglib/trueaudiofile.h>
|
||||
#include <taglib/vorbisfile.h>
|
||||
#include <taglib/wavfile.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
#include <taglib/dsffile.h>
|
||||
#endif
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions()
|
||||
{
|
||||
static const std::vector<std::filesystem::path> supportedExtensions{
|
||||
".mp3", ".mp2", ".aac", ".ogg", ".oga", ".flac", ".spx", ".opus",
|
||||
".mpc", ".wv", ".ape", ".tta", ".m4a", ".m4r", ".m4b", ".m4p",
|
||||
".3g2", ".m4v", ".wma", ".asf", ".aif", ".aiff", ".afc", ".aifc",
|
||||
".wav",
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
".dsf"
|
||||
#endif
|
||||
};
|
||||
|
||||
return std::span<const std::filesystem::path>{ supportedExtensions };
|
||||
}
|
||||
|
||||
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserOptions::ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
switch (readStyle)
|
||||
{
|
||||
case ParserOptions::AudioPropertiesReadStyle::Fast:
|
||||
return TagLib::AudioProperties::ReadStyle::Fast;
|
||||
case ParserOptions::AudioPropertiesReadStyle::Average:
|
||||
return TagLib::AudioProperties::ReadStyle::Average;
|
||||
case ParserOptions::AudioPropertiesReadStyle::Accurate:
|
||||
return TagLib::AudioProperties::ReadStyle::Accurate;
|
||||
}
|
||||
|
||||
throw Exception{ "Cannot convert read style" };
|
||||
}
|
||||
|
||||
TagLib::FileStream createFileStream(const std::filesystem::path& p)
|
||||
{
|
||||
FILE* file{ std::fopen(p.c_str(), "r") };
|
||||
if (!file)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fopen failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fopen failed", ec };
|
||||
}
|
||||
|
||||
int fd{ ::fileno(file) };
|
||||
if (fd == -1)
|
||||
{
|
||||
const std::error_code ec{ errno, std::generic_category() };
|
||||
LMS_LOG(METADATA, DEBUG, "fileno failed for " << p << ": " << ec.message());
|
||||
throw IOException{ "fileno failed", ec };
|
||||
}
|
||||
|
||||
return TagLib::FileStream{ fd, true };
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByExtension(TagLib::FileStream* stream, const std::filesystem::path& extension, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
|
||||
{
|
||||
constexpr bool readAudioProperties{ true };
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
if (extension.empty())
|
||||
return file;
|
||||
|
||||
const std::string ext{ core::stringUtils::stringToUpper(extension.string().substr(1)) };
|
||||
|
||||
// MP3
|
||||
if (ext == "MP3" || ext == "MP2" || ext == "AAC")
|
||||
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
// VORBIS
|
||||
else if (ext == "OGG")
|
||||
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "OGA")
|
||||
{
|
||||
/* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */
|
||||
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if (!file->isValid())
|
||||
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
else if (ext == "FLAC")
|
||||
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "SPX")
|
||||
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "OPUS")
|
||||
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// APE
|
||||
else if (ext == "MPC")
|
||||
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "WV")
|
||||
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "APE")
|
||||
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// TRUEAUDIO
|
||||
else if (ext == "TTA")
|
||||
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// MP4
|
||||
else if (ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V")
|
||||
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// ASF
|
||||
else if (ext == "WMA" || ext == "ASF")
|
||||
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// RIFF
|
||||
else if (ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC")
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (ext == "WAV")
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (ext == "DSF")
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
file.reset();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFileByContent(TagLib::FileStream* stream, TagLib::AudioProperties::ReadStyle audioPropertiesStyle)
|
||||
{
|
||||
constexpr bool readAudioProperties{ true };
|
||||
std::unique_ptr<TagLib::File> file;
|
||||
|
||||
if (TagLib::MPEG::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MPEG::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
// VORBIS
|
||||
else if (TagLib::Ogg::Vorbis::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Vorbis::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::FLAC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::FLAC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::FLAC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::FLAC::File>(stream, TagLib::ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::Speex::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Speex::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::Ogg::Opus::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::Ogg::Opus::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// APE
|
||||
else if (TagLib::MPC::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MPC::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::WavPack::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::WavPack::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::APE::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::APE::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// TRUEAUDIO
|
||||
else if (TagLib::TrueAudio::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::TrueAudio::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// MP4
|
||||
else if (TagLib::MP4::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::MP4::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
//_ASF
|
||||
else if (TagLib::ASF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::ASF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
// RIFF
|
||||
else if (TagLib::RIFF::AIFF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::RIFF::AIFF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if (TagLib::RIFF::WAV::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::RIFF::WAV::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#if LMS_TAGLIB_HAS_DSF
|
||||
else if (TagLib::DSF::File::isSupported(stream))
|
||||
file = std::make_unique<TagLib::DSF::File>(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
|
||||
if (file && !file->isValid())
|
||||
file.reset();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
|
||||
|
||||
const ::TagLib::AudioProperties::ReadStyle tagLibReadStyle{ readStyleToTagLibReadStyle(readStyle) };
|
||||
TagLib::FileStream fileStream{ createFileStream(p) };
|
||||
std::unique_ptr<TagLib::File> file{ parseFileByExtension(&fileStream, p.extension(), tagLibReadStyle) };
|
||||
if (!file)
|
||||
{
|
||||
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by extension");
|
||||
file = parseFileByContent(&fileStream, tagLibReadStyle);
|
||||
if (!file)
|
||||
LMS_LOG(METADATA, DEBUG, "File " << p << ": failed to parse by content");
|
||||
}
|
||||
|
||||
if (!file)
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw AudioFileParsingException{ p, "Parsing failed" };
|
||||
}
|
||||
|
||||
if (!file->audioProperties())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||
throw AudioFileNoAudioPropertiesException{ p };
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
} // namespace lms::audio::taglib::utils
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <taglib/tfile.h>
|
||||
|
||||
#include "audio/IAudioFileInfo.hpp"
|
||||
|
||||
namespace lms::audio::taglib::utils
|
||||
{
|
||||
std::span<const std::filesystem::path> getSupportedExtensions();
|
||||
std::unique_ptr<::TagLib::File> parseFile(const std::filesystem::path& p, ParserOptions::AudioPropertiesReadStyle readStyle);
|
||||
} // namespace lms::audio::taglib::utils
|
||||
Reference in New Issue
Block a user