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