1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2 * (c)LGPL2+
3 *
4 * Flacon - audio File Encoder
5 * https://github.com/flacon/flacon
6 *
7 * Copyright: 2021
8 * Alexander Sokoloff <sokoloff.a@gmail.com>
9 *
10 * This library is free software; you can redistribute it and/or
11 * modify it under the terms of the GNU Lesser General Public
12 * License as published by the Free Software Foundation; either
13 * version 2.1 of the License, or (at your option) any later version.
14
15 * This library is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * Lesser General Public License for more details.
19
20 * You should have received a copy of the GNU Lesser General Public
21 * License along with this library; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 *
24 * END_COMMON_COPYRIGHT_HEADER */
25
26 #include "audiofilematcher.h"
27 #include "formats_in/informat.h"
28 #include <QDir>
29 #include <QLoggingCategory>
30
31 namespace {
32 Q_LOGGING_CATEGORY(LOG, "AudioFileMatcher")
33 }
34
AudioFileMatcher(const QString & cueFilePath,const Tracks & tracks)35 AudioFileMatcher::AudioFileMatcher(const QString &cueFilePath, const Tracks &tracks) :
36 mCueFilePath(cueFilePath),
37 mTracks(tracks)
38 {
39 fillFileTags();
40
41 qCDebug(LOG) << "mFileTags =" << mFileTags;
42
43 QDir dir = QFileInfo(mCueFilePath).dir();
44 QStringList exts = InputFormat::allFileExts();
45 mAllAudioFiles = dir.entryInfoList(exts, QDir::Files | QDir::Readable);
46
47 for (const auto &fi : qAsConst(mAllAudioFiles)) {
48 qDebug(LOG) << "mAllAudioFiles: " << fi.filePath();
49 }
50
51 if (mAllAudioFiles.isEmpty() || mTracks.isEmpty()) {
52 return;
53 }
54
55 // Trivial, but frequent case. Directory contains only one audio file.
56 if (mFileTags.count() == 1 && mAllAudioFiles.count() == 1) {
57 mResult[mFileTags.first()] << mAllAudioFiles.first().filePath();
58 qCDebug(LOG) << "Return trivial:" << mResult;
59 return;
60 }
61
62 // Looks like this is a per-track album .....
63 if (mFileTags.count() == mAllAudioFiles.count()) {
64 for (const Track &track : qAsConst(mTracks)) {
65 mResult[track.tag(TagId::File)] = matchAudioFilesByTrack(track.tag(TagId::File), track.tag(TagId::Title));
66 }
67 qCDebug(LOG) << "Return per-track album:" << mResult;
68 return;
69 }
70
71 // Common search ............................
72 for (const QString &fileTag : qAsConst(mFileTags)) {
73 mResult[fileTag] = matchAudioFiles(fileTag);
74 }
75 qCDebug(LOG) << "Return common:" << mResult;
76 }
77
audioFiles(int index) const78 QStringList AudioFileMatcher::audioFiles(int index) const
79 {
80 QString tag = mFileTags[index];
81 return audioFiles(tag);
82 }
83
containsAudioFile(const QString & audioFile) const84 bool AudioFileMatcher::containsAudioFile(const QString &audioFile) const
85 {
86 for (auto const &files : qAsConst(mResult)) {
87 if (files.contains(audioFile)) {
88 return true;
89 }
90 }
91
92 return false;
93 }
94
fillFileTags()95 void AudioFileMatcher::fillFileTags()
96 {
97 QString prev;
98 for (const Track &track : qAsConst(mTracks)) {
99 if (track.tag(TagId::File) != prev) {
100 prev = track.tag(TagId::File);
101 mFileTags << track.tag(TagId::File);
102 }
103 }
104 }
105
matchAudioFilesByTrack(const QString & fileTag,const QString & trackTitle)106 QStringList AudioFileMatcher::matchAudioFilesByTrack(const QString &fileTag, const QString &trackTitle)
107 {
108 QStringList res;
109 QStringList patterns;
110
111 patterns << QRegExp::escape(QFileInfo(fileTag).completeBaseName());
112 patterns << QString(".*%1.*").arg(QRegExp::escape(trackTitle));
113
114 foreach (const QString &pattern, patterns) {
115 QRegExp re(QString(pattern), Qt::CaseInsensitive, QRegExp::RegExp2);
116
117 foreach (const QFileInfo &audio, mAllAudioFiles) {
118 if (re.exactMatch(audio.fileName())) {
119 res << audio.filePath();
120 }
121 }
122 }
123
124 if (res.isEmpty()) {
125 res = matchAudioFiles(fileTag);
126 }
127
128 res.removeDuplicates();
129 return res;
130 }
131
matchAudioFiles(const QString & fileTag)132 QStringList AudioFileMatcher::matchAudioFiles(const QString &fileTag)
133 {
134 QStringList res;
135
136 QStringList patterns;
137 if (mFileTags.count() == 1) {
138 patterns << QRegExp::escape(QFileInfo(mFileTags.first()).completeBaseName());
139 patterns << QRegExp::escape(QFileInfo(mCueFilePath).completeBaseName()) + ".*";
140 }
141 else {
142 int fileTagNum = mFileTags.indexOf(fileTag) + 1; // Disks are indexed from 1, not from 0!
143 patterns << QRegExp::escape(QFileInfo(fileTag).completeBaseName());
144 patterns << QRegExp::escape(QFileInfo(mCueFilePath).completeBaseName()) + QString("(.*\\D)?"
145 "0*"
146 "%1"
147 "(.*\\D)?")
148 .arg(fileTagNum);
149 patterns << QString(".*"
150 "(disk|disc|side)"
151 "(.*\\D)?"
152 "0*"
153 "%1"
154 "(.*\\D)?")
155 .arg(fileTagNum);
156 }
157
158 qCDebug(LOG) << "matchAudioFiles: patterns=" << patterns;
159 QString audioExt;
160 foreach (const InputFormat *format, InputFormat::allFormats()) {
161 audioExt += (audioExt.isEmpty() ? "\\." : "|\\.") + format->ext();
162 }
163
164 foreach (const QString &pattern, patterns) {
165 QRegExp re(QString("%1(%2)+").arg(pattern).arg(audioExt), Qt::CaseInsensitive, QRegExp::RegExp2);
166 foreach (const QFileInfo &audio, mAllAudioFiles) {
167 qCDebug(LOG) << "matchAudioFiles test: re=" << re << "file=" << audio.filePath();
168 if (re.exactMatch(audio.fileName())) {
169 res << audio.filePath();
170 qCDebug(LOG) << "matchAudioFiles test: MATCH";
171 }
172 }
173 }
174
175 res.removeDuplicates();
176 return res;
177 }
178