mirror of
https://github.com/strawberrymusicplayer/strawberry
synced 2025-02-02 10:36:45 +01:00
Initial support for GME's VGM/SPC playback and tag management.
Co-Authored-By: Jonas Kvinge <jonas@jkvinge.net>
This commit is contained in:
parent
6bc46e4598
commit
80da565609
@ -35,9 +35,6 @@
|
||||
|
||||
class QIODevice;
|
||||
|
||||
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
|
||||
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
|
||||
|
||||
// Reads and writes uint32 length encoded protobufs to a socket.
|
||||
// This base QObject is separate from AbstractMessageHandler because moc can't handle templated classes.
|
||||
// Use AbstractMessageHandler instead.
|
||||
|
@ -4,7 +4,7 @@ set(MESSAGES tagreadermessages.proto)
|
||||
set(SOURCES tagreaderbase.cpp)
|
||||
|
||||
if(USE_TAGLIB AND TAGLIB_FOUND)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp)
|
||||
list(APPEND SOURCES tagreadertaglib.cpp tagreadergme.cpp)
|
||||
endif()
|
||||
|
||||
if(USE_TAGPARSER AND TAGPARSER_FOUND)
|
||||
|
@ -26,6 +26,12 @@ const std::string TagReaderBase::kEmbeddedCover = "(embedded)";
|
||||
TagReaderBase::TagReaderBase() = default;
|
||||
TagReaderBase::~TagReaderBase() = default;
|
||||
|
||||
void TagReaderBase::Decode(const QString &tag, std::string *output) {
|
||||
|
||||
output->assign(DataCommaSizeFromQString(tag));
|
||||
|
||||
}
|
||||
|
||||
float TagReaderBase::ConvertPOPMRating(const int POPM_rating) {
|
||||
|
||||
if (POPM_rating < 0x01) return 0.0F;
|
||||
|
@ -27,6 +27,9 @@
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
|
||||
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
|
||||
|
||||
/*
|
||||
* This class holds all useful methods to read and write tags from/to files.
|
||||
* You should not use it directly in the main process but rather use a TagReaderWorker process (using TagReaderClient)
|
||||
@ -38,7 +41,7 @@ class TagReaderBase {
|
||||
|
||||
virtual bool IsMediaFile(const QString &filename) const = 0;
|
||||
|
||||
virtual void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const = 0;
|
||||
virtual bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
|
||||
virtual QByteArray LoadEmbeddedArt(const QString &filename) const = 0;
|
||||
@ -47,6 +50,8 @@ class TagReaderBase {
|
||||
virtual bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
virtual bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const = 0;
|
||||
|
||||
static void Decode(const QString &tag, std::string *output);
|
||||
|
||||
static float ConvertPOPMRating(const int POPM_rating);
|
||||
static int ConvertToPOPMRating(const float rating);
|
||||
|
||||
|
299
ext/libstrawberry-tagreader/tagreadergme.cpp
Normal file
299
ext/libstrawberry-tagreader/tagreadergme.cpp
Normal file
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2022, Eoin O'Neill <eoinoneill1991@gmail.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "tagreadergme.h"
|
||||
|
||||
#include <tag.h>
|
||||
#include <apefile.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QChar>
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadertaglib.h"
|
||||
|
||||
bool GME::IsSupportedFormat(const QFileInfo &file_info) {
|
||||
return file_info.exists() && (file_info.completeSuffix().endsWith("spc") || file_info.completeSuffix().endsWith("vgm"));
|
||||
}
|
||||
|
||||
bool GME::ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
if (file_info.completeSuffix().endsWith("spc")) {
|
||||
SPC::Read(file_info, song_info);
|
||||
return true;
|
||||
}
|
||||
if (file_info.completeSuffix().endsWith("vgm")) {
|
||||
VGM::Read(file_info, song_info);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
quint32 GME::UnpackBytes32(const char *const bytes, size_t length) {
|
||||
|
||||
Q_ASSERT(length <= 4 && length > 0);
|
||||
|
||||
quint32 value = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
value |= static_cast<unsigned char>(bytes[i]) << (8 * i);
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
}
|
||||
|
||||
void GME::SPC::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
QFile file(file_info.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) return;
|
||||
|
||||
qLog(Debug) << "Reading tags from SPC file" << file_info.fileName();
|
||||
|
||||
// Check for header -- more reliable than file name alone.
|
||||
if (!file.read(33).startsWith(QString("SNES-SPC700").toLatin1())) return;
|
||||
|
||||
// First order of business -- get any tag values that exist within the core file information.
|
||||
// These only allow for a certain number of bytes per field,
|
||||
// so they will likely be overwritten either by the id666 standard or the APETAG format (as used by other players, such as foobar and winamp)
|
||||
//
|
||||
// Make sure to check id6 documentation before changing the read values!
|
||||
|
||||
file.seek(HAS_ID6_OFFSET);
|
||||
bool has_id6 = (file.read(1)[0] == static_cast<char>(xID6_STATUS::ON));
|
||||
|
||||
file.seek(SONG_TITLE_OFFSET);
|
||||
song_info->set_title(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(GAME_TITLE_OFFSET);
|
||||
song_info->set_album(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(ARTIST_OFFSET);
|
||||
song_info->set_artist(QString::fromLatin1(file.read(32)).toStdString());
|
||||
|
||||
file.seek(INTRO_LENGTH_OFFSET);
|
||||
QByteArray length_bytes = file.read(INTRO_LENGTH_SIZE);
|
||||
quint64 length_in_sec = 0;
|
||||
if (length_bytes.size() >= INTRO_LENGTH_SIZE) {
|
||||
length_in_sec = ConvertSPCStringToNum(length_bytes);
|
||||
|
||||
if (!length_in_sec || length_in_sec >= 0x1FFF) {
|
||||
// This means that parsing the length as a string failed, so get value LE.
|
||||
length_in_sec = length_bytes[0] | (length_bytes[1] << 8) | (length_bytes[2] << 16);
|
||||
}
|
||||
|
||||
if (length_in_sec < 0x1FFF) {
|
||||
song_info->set_length_nanosec(length_in_sec * kNsecPerSec);
|
||||
}
|
||||
}
|
||||
|
||||
file.seek(FADE_LENGTH_OFFSET);
|
||||
QByteArray fade_bytes = file.read(FADE_LENGTH_SIZE);
|
||||
if (fade_bytes.size() >= FADE_LENGTH_SIZE) {
|
||||
quint64 fade_length_in_ms = ConvertSPCStringToNum(fade_bytes);
|
||||
|
||||
if (fade_length_in_ms > 0x7FFF) {
|
||||
fade_length_in_ms = fade_bytes[0] | (fade_bytes[1] << 8) | (fade_bytes[2] << 16) | (fade_bytes[3] << 24);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for XID6 data -- this is infrequently used, but being able to fill in data from this is ideal before trying to rely on APETAG values.
|
||||
// XID6 format follows EA's binary file format standard named "IFF"
|
||||
file.seek(XID6_OFFSET);
|
||||
if (has_id6 && file.read(4) == QString("xid6").toLatin1()) {
|
||||
QByteArray xid6_head_data = file.read(4);
|
||||
if (xid6_head_data.size() >= 4) {
|
||||
qint64 xid6_size = xid6_head_data[0] | (xid6_head_data[1] << 8) | (xid6_head_data[2] << 16) | xid6_head_data[3];
|
||||
// This should be the size remaining for entire ID6 block, but it seems that most files treat this as the size of the remaining header space...
|
||||
|
||||
qLog(Debug) << file_info.fileName() << "has ID6 tag.";
|
||||
|
||||
while ((file.pos()) + 4 < XID6_OFFSET + xid6_size) {
|
||||
QByteArray arr = file.read(4);
|
||||
if (arr.size() < 4) break;
|
||||
|
||||
qint8 id = arr[0];
|
||||
qint8 type = arr[1];
|
||||
Q_UNUSED(id);
|
||||
Q_UNUSED(type);
|
||||
qint16 length = arr[2] | (arr[3] << 8);
|
||||
|
||||
file.read(GetNextMemAddressAlign32bit(length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Music Players that support SPC tend to support additional tagging data as
|
||||
// an APETAG entry at the bottom of the file instead of writing into the xid6 tagging space.
|
||||
// This is where a lot of the extra data for a file is stored, such as genre or replaygain data.
|
||||
// This data is currently supported by TagLib, so we will simply use that for the remaining values.
|
||||
TagLib::APE::File ape(file_info.filePath().toStdString().data());
|
||||
if (ape.hasAPETag()) {
|
||||
TagLib::Tag *tag = ape.tag();
|
||||
if (!tag) return;
|
||||
|
||||
TagReaderTagLib::Decode(tag->artist(), song_info->mutable_artist());
|
||||
TagReaderTagLib::Decode(tag->album(), song_info->mutable_album());
|
||||
TagReaderTagLib::Decode(tag->title(), song_info->mutable_title());
|
||||
TagReaderTagLib::Decode(tag->genre(), song_info->mutable_genre());
|
||||
song_info->set_track(tag->track());
|
||||
song_info->set_year(tag->year());
|
||||
}
|
||||
|
||||
song_info->set_valid(true);
|
||||
song_info->set_filetype(spb::tagreader::SongMetadata_FileType_SPC);
|
||||
|
||||
}
|
||||
|
||||
qint16 GME::SPC::GetNextMemAddressAlign32bit(qint16 input) {
|
||||
return ((input + 0x3) & ~0x3);
|
||||
// Plus 0x3 for rounding up (not down), AND NOT to flatten out on a 32 bit level.
|
||||
}
|
||||
|
||||
quint64 GME::SPC::ConvertSPCStringToNum(const QByteArray &arr) {
|
||||
|
||||
quint64 result = 0;
|
||||
for (auto it = arr.begin(); it != arr.end(); it++) {
|
||||
unsigned int num = *it - '0';
|
||||
if (num > 9) break;
|
||||
result = (result * 10) + num; // Shift Left and add.
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
void GME::VGM::Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info) {
|
||||
|
||||
QFile file(file_info.filePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) return;
|
||||
|
||||
qLog(Debug) << "Reading tags from VGM file" << file_info.fileName();
|
||||
|
||||
if (!file.read(4).startsWith(QString("Vgm ").toLatin1())) return;
|
||||
|
||||
file.seek(GD3_TAG_PTR);
|
||||
QByteArray gd3_head = file.read(4);
|
||||
if (gd3_head.size() < 4) return;
|
||||
|
||||
quint64 pt = GME::UnpackBytes32(gd3_head, gd3_head.size());
|
||||
|
||||
file.seek(SAMPLE_COUNT);
|
||||
QByteArray sample_count_bytes = file.read(4);
|
||||
file.seek(LOOP_SAMPLE_COUNT);
|
||||
QByteArray loop_count_bytes = file.read(4);
|
||||
quint64 length = 0;
|
||||
|
||||
if (!GetPlaybackLength(sample_count_bytes, loop_count_bytes, length)) return;
|
||||
|
||||
file.seek(GD3_TAG_PTR + pt);
|
||||
QByteArray gd3_version = file.read(4);
|
||||
|
||||
file.seek(file.pos() + 4);
|
||||
QByteArray gd3_length_bytes = file.read(4);
|
||||
quint32 gd3_length = GME::UnpackBytes32(gd3_length_bytes, gd3_length_bytes.size());
|
||||
|
||||
QByteArray gd3Data = file.read(gd3_length);
|
||||
QTextStream fileTagStream(gd3Data, QIODevice::ReadOnly);
|
||||
// Stored as 16 bit UTF string, two bytes per letter.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
fileTagStream.setEncoding(QStringConverter::Utf16);
|
||||
#else
|
||||
fileTagStream.setCodec("UTF-8");
|
||||
#endif
|
||||
QStringList strings = fileTagStream.readLine(0).split(QChar('\0'));
|
||||
if (strings.count() < 10) return;
|
||||
|
||||
// VGM standard dictates string tag data exist in specific order.
|
||||
// Order alternates between English and Japanese version of data.
|
||||
// Read GD3 tag standard for more details.
|
||||
song_info->set_title(strings[0].toStdString());
|
||||
song_info->set_album(strings[2].toStdString());
|
||||
song_info->set_artist(strings[6].toStdString());
|
||||
song_info->set_year(strings[8].left(4).toInt());
|
||||
song_info->set_length_nanosec(length * kNsecPerMsec);
|
||||
song_info->set_valid(true);
|
||||
song_info->set_filetype(spb::tagreader::SongMetadata_FileType_VGM);
|
||||
|
||||
}
|
||||
|
||||
bool GME::VGM::GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length) {
|
||||
|
||||
if (sample_count_bytes.size() != 4) return false;
|
||||
if (loop_count_bytes.size() != 4) return false;
|
||||
|
||||
quint64 sample_count = GME::UnpackBytes32(sample_count_bytes, sample_count_bytes.size());
|
||||
|
||||
if (sample_count <= 0) return false;
|
||||
|
||||
quint64 loop_sample_count = GME::UnpackBytes32(loop_count_bytes, loop_count_bytes.size());
|
||||
|
||||
if (loop_sample_count <= 0) {
|
||||
out_length = sample_count * 1000 / SAMPLE_TIMEBASE;
|
||||
return true;
|
||||
}
|
||||
|
||||
quint64 intro_length_ms = (sample_count - loop_sample_count) * 1000 / SAMPLE_TIMEBASE;
|
||||
quint64 loop_length_ms = (loop_sample_count) * 1000 / SAMPLE_TIMEBASE;
|
||||
out_length = intro_length_ms + (loop_length_ms * 2) + GST_GME_LOOP_TIME_MS;
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
TagReaderGME::TagReaderGME() = default;
|
||||
TagReaderGME::~TagReaderGME() = default;
|
||||
|
||||
bool TagReaderGME::IsMediaFile(const QString &filename) const {
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::IsSupportedFormat(fileinfo);
|
||||
}
|
||||
|
||||
bool TagReaderGME::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
QFileInfo fileinfo(filename);
|
||||
return GME::ReadFile(fileinfo, song);
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveFile(const QString&, const spb::tagreader::SongMetadata&) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray TagReaderGME::LoadEmbeddedArt(const QString&) const {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveEmbeddedArt(const QString&, const QByteArray&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveSongPlaycountToFile(const QString&, const spb::tagreader::SongMetadata&) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TagReaderGME::SaveSongRatingToFile(const QString&, const spb::tagreader::SongMetadata&) const {
|
||||
return false;
|
||||
}
|
111
ext/libstrawberry-tagreader/tagreadergme.h
Normal file
111
ext/libstrawberry-tagreader/tagreadergme.h
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2022, Eoin O'Neill <eoinoneill1991@gmail.com>
|
||||
*
|
||||
* Strawberry 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.
|
||||
*
|
||||
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef TAGREADERGME_H
|
||||
#define TAGREADERGME_H
|
||||
|
||||
#include <taglib/tstring.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QFileInfo>
|
||||
|
||||
#include "tagreaderbase.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
|
||||
namespace GME {
|
||||
bool IsSupportedFormat(const QFileInfo &file_info);
|
||||
bool ReadFile(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
|
||||
uint32_t UnpackBytes32(const char *const arr, size_t length);
|
||||
|
||||
namespace SPC {
|
||||
// SPC SPEC: http://vspcplay.raphnet.net/spc_file_format.txt
|
||||
|
||||
constexpr int HAS_ID6_OFFSET = 0x23;
|
||||
constexpr int SONG_TITLE_OFFSET = 0x2E;
|
||||
constexpr int GAME_TITLE_OFFSET = 0x4E;
|
||||
constexpr int DUMPER_OFFSET = 0x6E;
|
||||
constexpr int COMMENTS_OFFSET = 0x7E;
|
||||
// It seems that intro length and fade length are inconsistent from file to file.
|
||||
// It should be looked into within the GME source code to see how GStreamer gets its values for playback length.
|
||||
constexpr int INTRO_LENGTH_OFFSET = 0xA9;
|
||||
constexpr int INTRO_LENGTH_SIZE = 3;
|
||||
constexpr int FADE_LENGTH_OFFSET = 0xAC;
|
||||
constexpr int FADE_LENGTH_SIZE = 4;
|
||||
constexpr int ARTIST_OFFSET = 0xB1;
|
||||
constexpr int XID6_OFFSET = (0x101C0 + 64);
|
||||
|
||||
constexpr int NANO_PER_MS = 1000000;
|
||||
|
||||
enum xID6_STATUS {
|
||||
ON = 0x26,
|
||||
OFF = 0x27,
|
||||
};
|
||||
|
||||
enum xID6_ID { SongName = 0x01, GameName = 0x02, ArtistName = 0x03 };
|
||||
|
||||
enum xID6_TYPE { Length = 0x0, String = 0x1, Integer = 0x4 };
|
||||
|
||||
void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
qint16 GetNextMemAddressAlign32bit(qint16 input);
|
||||
quint64 ConvertSPCStringToNum(const QByteArray &arr);
|
||||
} // namespace SPC
|
||||
|
||||
namespace VGM {
|
||||
// VGM SPEC:
|
||||
// http://www.smspower.org/uploads/Music/vgmspec170.txt?sid=17c810c54633b6dd4982f92f718361c1
|
||||
// GD3 TAG SPEC:
|
||||
// http://www.smspower.org/uploads/Music/gd3spec100.txt
|
||||
constexpr int GD3_TAG_PTR = 0x14;
|
||||
constexpr int SAMPLE_COUNT = 0x18;
|
||||
constexpr int LOOP_SAMPLE_COUNT = 0x20;
|
||||
constexpr int SAMPLE_TIMEBASE = 44100;
|
||||
constexpr int GST_GME_LOOP_TIME_MS = 8000;
|
||||
|
||||
void Read(const QFileInfo &file_info, spb::tagreader::SongMetadata *song_info);
|
||||
// Takes in two QByteArrays, expected to be 4 bytes long. Desired length is returned via output parameter out_length. Returns false on error.
|
||||
bool GetPlaybackLength(const QByteArray &sample_count_bytes, const QByteArray &loop_count_bytes, quint64 &out_length);
|
||||
|
||||
} // namespace VGM
|
||||
|
||||
} // namespace GME
|
||||
|
||||
// TagReaderGME
|
||||
// Fulfills Strawberry's Intended interface for tag reading.
|
||||
class TagReaderGME : public TagReaderBase {
|
||||
|
||||
public:
|
||||
explicit TagReaderGME();
|
||||
~TagReaderGME();
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
bool SaveEmbeddedArt(const QString &filename, const QByteArray &data) override;
|
||||
|
||||
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
};
|
||||
|
||||
#endif
|
@ -27,6 +27,8 @@ message SongMetadata {
|
||||
S3M = 19;
|
||||
XM = 20;
|
||||
IT = 21;
|
||||
SPC = 22;
|
||||
VGM = 23;
|
||||
CDDA = 90;
|
||||
STREAM = 91;
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ spb::tagreader::SongMetadata_FileType TagReaderTagLib::GuessFileType(TagLib::Fil
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
const QFileInfo fileinfo(filename);
|
||||
@ -212,7 +212,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
|
||||
std::unique_ptr<TagLib::FileRef> fileref(factory_->GetFileRef(filename));
|
||||
if (fileref->isNull()) {
|
||||
qLog(Info) << "TagLib hasn't been able to read" << filename << "file";
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
song->set_filetype(GuessFileType(fileref.get()));
|
||||
@ -254,9 +254,7 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
|
||||
}
|
||||
|
||||
if (TagLib::FLAC::File *file_flac = dynamic_cast<TagLib::FLAC::File *>(fileref->file())) {
|
||||
|
||||
song->set_bitdepth(file_flac->audioProperties()->bitsPerSample());
|
||||
|
||||
if (file_flac->xiphComment()) {
|
||||
ParseOggTag(file_flac->xiphComment()->fieldListMap(), &disc, &compilation, song);
|
||||
TagLib::List<TagLib::FLAC::Picture*> pictures = file_flac->pictureList();
|
||||
@ -517,6 +515,8 @@ void TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
|
||||
if (song->bitrate() <= 0) { song->set_bitrate(-1); }
|
||||
if (song->lastplayed() <= 0) { song->set_lastplayed(-1); }
|
||||
|
||||
return song->filetype() != spb::tagreader::SongMetadata_FileType_UNKNOWN;
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) {
|
||||
@ -526,12 +526,6 @@ void TagReaderTagLib::Decode(const TagLib::String &tag, std::string *output) {
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagLib::Decode(const QString &tag, std::string *output) {
|
||||
|
||||
output->assign(DataCommaSizeFromQString(tag));
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
if (!map["COMPOSER"].isEmpty()) Decode(map["COMPOSER"].front(), song->mutable_composer());
|
||||
|
@ -50,7 +50,7 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
@ -59,12 +59,11 @@ class TagReaderTagLib : public TagReaderBase {
|
||||
bool SaveSongPlaycountToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
bool SaveSongRatingToFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
static void Decode(const TagLib::String &tag, std::string *output);
|
||||
|
||||
private:
|
||||
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
|
||||
|
||||
static void Decode(const TagLib::String &tag, std::string *output);
|
||||
static void Decode(const QString &tag, std::string *output);
|
||||
|
||||
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
|
||||
|
||||
|
@ -93,13 +93,13 @@ bool TagReaderTagParser::IsMediaFile(const QString &filename) const {
|
||||
|
||||
}
|
||||
|
||||
void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
bool TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const {
|
||||
|
||||
qLog(Debug) << "Reading tags from" << filename;
|
||||
|
||||
const QFileInfo fileinfo(filename);
|
||||
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return;
|
||||
if (!fileinfo.exists() || fileinfo.suffix().compare("bak", Qt::CaseInsensitive) == 0) return false;
|
||||
|
||||
const QByteArray url(QUrl::fromLocalFile(filename).toEncoded());
|
||||
|
||||
@ -135,19 +135,19 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
taginfo.parseContainerFormat(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTracks(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
taginfo.parseTags(diag, progress);
|
||||
if (progress.isAborted()) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const TagParser::DiagMessage &msg : diag) {
|
||||
@ -206,7 +206,7 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
|
||||
if (song->filetype() == spb::tagreader::SongMetadata_FileType::SongMetadata_FileType_UNKNOWN) {
|
||||
taginfo.close();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto tag : taginfo.tags()) {
|
||||
@ -247,8 +247,12 @@ void TagReaderTagParser::ReadFile(const QString &filename, spb::tagreader::SongM
|
||||
|
||||
taginfo.close();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
catch(...) {
|
||||
return false;
|
||||
}
|
||||
catch(...) {}
|
||||
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ class TagReaderTagParser : public TagReaderBase {
|
||||
|
||||
bool IsMediaFile(const QString &filename) const override;
|
||||
|
||||
void ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool ReadFile(const QString &filename, spb::tagreader::SongMetadata *song) const override;
|
||||
bool SaveFile(const QString &filename, const spb::tagreader::SongMetadata &song) const override;
|
||||
|
||||
QByteArray LoadEmbeddedArt(const QString &filename) const override;
|
||||
|
@ -34,28 +34,11 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
|
||||
spb::tagreader::Message reply;
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
reply.mutable_is_media_file_response()->set_success(tag_reader_.IsMediaFile(QStringFromStdString(message.is_media_file_request().filename())));
|
||||
}
|
||||
else if (message.has_read_file_request()) {
|
||||
tag_reader_.ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
|
||||
}
|
||||
else if (message.has_save_file_request()) {
|
||||
reply.mutable_save_file_response()->set_success(tag_reader_.SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata()));
|
||||
}
|
||||
else if (message.has_load_embedded_art_request()) {
|
||||
QByteArray data = tag_reader_.LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
|
||||
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
|
||||
}
|
||||
else if (message.has_save_embedded_art_request()) {
|
||||
reply.mutable_save_embedded_art_response()->set_success(tag_reader_.SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast<qint64>(message.save_embedded_art_request().data().size()))));
|
||||
}
|
||||
|
||||
else if (message.has_save_song_playcount_to_file_request()) {
|
||||
reply.mutable_save_song_playcount_to_file_response()->set_success(tag_reader_.SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata()));
|
||||
}
|
||||
else if (message.has_save_song_rating_to_file_request()) {
|
||||
reply.mutable_save_song_rating_to_file_response()->set_success(tag_reader_.SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata()));
|
||||
bool success = HandleMessage(message, reply, &tag_reader_);
|
||||
if (!success) {
|
||||
#if defined(USE_TAGLIB)
|
||||
HandleMessage(message, reply, &tag_reader_gme_);
|
||||
#endif
|
||||
}
|
||||
|
||||
SendReply(message, &reply);
|
||||
@ -63,7 +46,50 @@ void TagReaderWorker::MessageArrived(const spb::tagreader::Message &message) {
|
||||
}
|
||||
|
||||
void TagReaderWorker::DeviceClosed() {
|
||||
|
||||
AbstractMessageHandler<spb::tagreader::Message>::DeviceClosed();
|
||||
|
||||
QCoreApplication::exit();
|
||||
|
||||
}
|
||||
|
||||
bool TagReaderWorker::HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase *reader) {
|
||||
|
||||
if (message.has_is_media_file_request()) {
|
||||
bool success = reader->IsMediaFile(QStringFromStdString(message.is_media_file_request().filename()));
|
||||
reply.mutable_is_media_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_read_file_request()) {
|
||||
bool success = reader->ReadFile(QStringFromStdString(message.read_file_request().filename()), reply.mutable_read_file_response()->mutable_metadata());
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_file_request()) {
|
||||
bool success = reader->SaveFile(QStringFromStdString(message.save_file_request().filename()), message.save_file_request().metadata());
|
||||
reply.mutable_save_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_load_embedded_art_request()) {
|
||||
QByteArray data = reader->LoadEmbeddedArt(QStringFromStdString(message.load_embedded_art_request().filename()));
|
||||
reply.mutable_load_embedded_art_response()->set_data(data.constData(), data.size());
|
||||
return true;
|
||||
}
|
||||
else if (message.has_save_embedded_art_request()) {
|
||||
bool success = reader->SaveEmbeddedArt(QStringFromStdString(message.save_embedded_art_request().filename()), QByteArray(message.save_embedded_art_request().data().data(), static_cast<qint64>(message.save_embedded_art_request().data().size())));
|
||||
reply.mutable_save_embedded_art_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_song_playcount_to_file_request()) {
|
||||
bool success = reader->SaveSongPlaycountToFile(QStringFromStdString(message.save_song_playcount_to_file_request().filename()), message.save_song_playcount_to_file_request().metadata());
|
||||
reply.mutable_save_song_playcount_to_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
else if (message.has_save_song_rating_to_file_request()) {
|
||||
bool success = reader->SaveSongRatingToFile(QStringFromStdString(message.save_song_rating_to_file_request().filename()), message.save_song_rating_to_file_request().metadata());
|
||||
reply.mutable_save_song_rating_to_file_response()->set_success(success);
|
||||
return success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
@ -26,9 +26,11 @@
|
||||
#include "core/messagehandler.h"
|
||||
#if defined(USE_TAGLIB)
|
||||
# include "tagreadertaglib.h"
|
||||
# include "tagreadergme.h"
|
||||
#elif defined(USE_TAGPARSER)
|
||||
# include "tagreadertagparser.h"
|
||||
#endif
|
||||
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
class QIODevice;
|
||||
@ -44,8 +46,12 @@ class TagReaderWorker : public AbstractMessageHandler<spb::tagreader::Message> {
|
||||
void DeviceClosed() override;
|
||||
|
||||
private:
|
||||
// Handle message using specific TagReaderBase implementation. Returns true on successful message handle.
|
||||
bool HandleMessage(const spb::tagreader::Message &message, spb::tagreader::Message &reply, TagReaderBase* reader);
|
||||
|
||||
#if defined(USE_TAGLIB)
|
||||
TagReaderTagLib tag_reader_;
|
||||
TagReaderGME tag_reader_gme_;
|
||||
#elif defined(USE_TAGPARSER)
|
||||
TagReaderTagParser tag_reader_;
|
||||
#endif
|
||||
|
@ -52,7 +52,6 @@
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/messagehandler.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include "engine/enginebase.h"
|
||||
@ -65,6 +64,9 @@
|
||||
#include "sqlrow.h"
|
||||
#include "tagreadermessages.pb.h"
|
||||
|
||||
#define QStringFromStdString(x) QString::fromUtf8((x).data(), (x).size())
|
||||
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
|
||||
|
||||
const QStringList Song::kColumns = QStringList() << "title"
|
||||
<< "album"
|
||||
<< "artist"
|
||||
@ -156,7 +158,7 @@ const QStringList Song::kArticles = QStringList() << "the " << "a " << "an ";
|
||||
const QStringList Song::kAcceptedExtensions = QStringList() << "wav" << "flac" << "wv" << "ogg" << "oga" << "opus" << "spx" << "ape" << "mpc"
|
||||
<< "mp2" << "mp3" << "m4a" << "mp4" << "aac" << "asf" << "asx" << "wma"
|
||||
<< "aif << aiff" << "mka" << "tta" << "dsf" << "dsd"
|
||||
<< "ac3" << "dts";
|
||||
<< "ac3" << "dts" << "spc" << "vgm";
|
||||
|
||||
struct Song::Private : public QSharedData {
|
||||
|
||||
@ -604,6 +606,8 @@ QString Song::TextForFiletype(FileType filetype) {
|
||||
case Song::FileType_XM: return "Module Music Format";
|
||||
case Song::FileType_IT: return "Module Music Format";
|
||||
case Song::FileType_CDDA: return "CDDA";
|
||||
case Song::FileType_SPC: return "SNES SPC700";
|
||||
case Song::FileType_VGM: return "VGM";
|
||||
case Song::FileType_Stream: return "Stream";
|
||||
case Song::FileType_Unknown:
|
||||
default: return QObject::tr("Unknown");
|
||||
@ -634,6 +638,8 @@ QString Song::ExtensionForFiletype(FileType filetype) {
|
||||
case Song::FileType_S3M: return "s3m";
|
||||
case Song::FileType_XM: return "xm";
|
||||
case Song::FileType_IT: return "it";
|
||||
case Song::FileType_SPC: return "spc";
|
||||
case Song::FileType_VGM: return "vgm";
|
||||
case Song::FileType_Unknown:
|
||||
default: return "dat";
|
||||
}
|
||||
@ -710,6 +716,8 @@ Song::FileType Song::FiletypeByMimetype(const QString &mimetype) {
|
||||
else if (mimetype.compare("audio/x-ape", Qt::CaseInsensitive) == 0 || mimetype.compare("application/x-ape", Qt::CaseInsensitive) == 0 || mimetype.compare("audio/x-ffmpeg-parsed-ape", Qt::CaseInsensitive) == 0) return Song::FileType_APE;
|
||||
else if (mimetype.compare("audio/x-mod", Qt::CaseInsensitive) == 0) return Song::FileType_MOD;
|
||||
else if (mimetype.compare("audio/x-s3m", Qt::CaseInsensitive) == 0) return Song::FileType_S3M;
|
||||
else if (mimetype.compare("audio/x-spc", Qt::CaseInsensitive) == 0) return Song::FileType_SPC;
|
||||
else if (mimetype.compare("audio/x-vgm", Qt::CaseInsensitive) == 0) return Song::FileType_VGM;
|
||||
|
||||
else return Song::FileType_Unknown;
|
||||
|
||||
@ -733,6 +741,8 @@ Song::FileType Song::FiletypeByDescription(const QString &text) {
|
||||
else if (text.compare("audio/x-ffmpeg-parsed-ape", Qt::CaseInsensitive) == 0) return Song::FileType_APE;
|
||||
else if (text.compare("Module Music Format (MOD)", Qt::CaseInsensitive) == 0) return Song::FileType_MOD;
|
||||
else if (text.compare("Module Music Format (MOD)", Qt::CaseInsensitive) == 0) return Song::FileType_S3M;
|
||||
else if (text.compare("SNES SPC700", Qt::CaseInsensitive) == 0) return Song::FileType_SPC;
|
||||
else if (text.compare("VGM", Qt::CaseInsensitive) == 0) return Song::FileType_VGM;
|
||||
else return Song::FileType_Unknown;
|
||||
|
||||
}
|
||||
@ -760,6 +770,8 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) {
|
||||
else if (ext.compare("s3m", Qt::CaseInsensitive) == 0) return Song::FileType_S3M;
|
||||
else if (ext.compare("xm", Qt::CaseInsensitive) == 0) return Song::FileType_XM;
|
||||
else if (ext.compare("it", Qt::CaseInsensitive) == 0) return Song::FileType_IT;
|
||||
else if (ext.compare("spc", Qt::CaseInsensitive) == 0) return Song::FileType_SPC;
|
||||
else if (ext.compare("vgm", Qt::CaseInsensitive) == 0) return Song::FileType_VGM;
|
||||
|
||||
else return Song::FileType_Unknown;
|
||||
|
||||
|
@ -106,6 +106,8 @@ class Song {
|
||||
FileType_S3M = 19,
|
||||
FileType_XM = 20,
|
||||
FileType_IT = 21,
|
||||
FileType_SPC = 22,
|
||||
FileType_VGM = 23,
|
||||
FileType_CDDA = 90,
|
||||
FileType_Stream = 91,
|
||||
};
|
||||
|
@ -39,6 +39,8 @@
|
||||
#include "tagreaderclient.h"
|
||||
#include "settings/collectionsettingspage.h"
|
||||
|
||||
#define DataCommaSizeFromQString(x) (x).toUtf8().constData(), (x).toUtf8().length()
|
||||
|
||||
const char *TagReaderClient::kWorkerExecutableName = "strawberry-tagreader";
|
||||
TagReaderClient *TagReaderClient::sInstance = nullptr;
|
||||
|
||||
|
@ -52,7 +52,8 @@ const char *FileView::kFileFilter =
|
||||
"*.aif *.aiff *.mka *.tta *.dsf *.dsd "
|
||||
"*.cue *.m3u *.m3u8 *.pls *.xspf *.asxini "
|
||||
"*.ac3 *.dts "
|
||||
"*.mod *.s3m *.xm *.it";
|
||||
"*.mod *.s3m *.xm *.it"
|
||||
"*.spc *.vgm";
|
||||
|
||||
FileView::FileView(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
|
Loading…
x
Reference in New Issue
Block a user