1 /* This file is part of Clementine.
2 Copyright 2010-2011, 2014, David Sansome <me@davidsansome.com>
3 Copyright 2011, Angus Gratton <gus@projectgus.com>
4 Copyright 2012, Mateusz Kowalczyk <mk440@bath.ac.uk>
5 Copyright 2013-2014, John Maguire <john.maguire@gmail.com>
6 Copyright 2014, Arnaud Bienner <arnaud.bienner@gmail.com>
7 Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
8
9 Clementine is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or
12 (at your option) any later version.
13
14 Clementine is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with Clementine. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23 #include "core/organiseformat.h"
24
25 #include <QApplication>
26 #include <QFileInfo>
27 #include <QPalette>
28 #include <QUrl>
29
30 #include "core/arraysize.h"
31 #include "core/timeconstants.h"
32 #include "core/utilities.h"
33
34 const char* OrganiseFormat::kTagPattern = "\\%([a-zA-Z]*)";
35 const char* OrganiseFormat::kBlockPattern = "\\{([^{}]+)\\}";
36 const QStringList OrganiseFormat::kKnownTags = QStringList() << "title"
37 << "album"
38 << "artist"
39 << "artistinitial"
40 << "albumartist"
41 << "composer"
42 << "track"
43 << "disc"
44 << "bpm"
45 << "year"
46 << "genre"
47 << "comment"
48 << "length"
49 << "bitrate"
50 << "samplerate"
51 << "extension"
52 << "performer"
53 << "grouping"
54 << "lyrics"
55 << "originalyear";
56
57 // From http://en.wikipedia.org/wiki/8.3_filename#Directory_table
58 const char OrganiseFormat::kInvalidFatCharacters[] = "\"*/\\:<>?|";
59 const int OrganiseFormat::kInvalidFatCharactersCount =
60 arraysize(OrganiseFormat::kInvalidFatCharacters) - 1;
61
62 const char OrganiseFormat::kInvalidPrefixCharacters[] = ".";
63 const int OrganiseFormat::kInvalidPrefixCharactersCount =
64 arraysize(OrganiseFormat::kInvalidPrefixCharacters) - 1;
65
66 const QRgb OrganiseFormat::SyntaxHighlighter::kValidTagColorLight =
67 qRgb(64, 64, 255);
68 const QRgb OrganiseFormat::SyntaxHighlighter::kInvalidTagColorLight =
69 qRgb(255, 64, 64);
70 const QRgb OrganiseFormat::SyntaxHighlighter::kBlockColorLight =
71 qRgb(230, 230, 230);
72
73 const QRgb OrganiseFormat::SyntaxHighlighter::kValidTagColorDark =
74 qRgb(128, 128, 255);
75 const QRgb OrganiseFormat::SyntaxHighlighter::kInvalidTagColorDark =
76 qRgb(255, 128, 128);
77 const QRgb OrganiseFormat::SyntaxHighlighter::kBlockColorDark =
78 qRgb(64, 64, 64);
79
OrganiseFormat(const QString & format)80 OrganiseFormat::OrganiseFormat(const QString& format)
81 : format_(format),
82 replace_non_ascii_(false),
83 replace_spaces_(false),
84 replace_the_(false) {}
85
set_format(const QString & v)86 void OrganiseFormat::set_format(const QString& v) {
87 format_ = v;
88 format_.replace('\\', '/');
89 }
90
IsValid() const91 bool OrganiseFormat::IsValid() const {
92 int pos = 0;
93 QString format_copy(format_);
94
95 Validator v;
96 return v.validate(format_copy, pos) == QValidator::Acceptable;
97 }
98
GetFilenameForSong(const Song & song) const99 QString OrganiseFormat::GetFilenameForSong(const Song& song) const {
100 QString filename = ParseBlock(format_, song);
101
102 if (QFileInfo(filename).completeBaseName().isEmpty()) {
103 // Avoid having empty filenames, or filenames with extension only: in this
104 // case, keep the original filename.
105 // We remove the extension from "filename" if it exists, as
106 // song.basefilename()
107 // also contains the extension.
108 filename =
109 Utilities::PathWithoutFilenameExtension(filename) + song.basefilename();
110 }
111
112 if (replace_spaces_) filename.replace(QRegExp("\\s"), "_");
113
114 if (replace_non_ascii_) {
115 QString stripped;
116 for (int i = 0; i < filename.length(); ++i) {
117 const QCharRef c = filename[i];
118 if (c < 128) {
119 stripped.append(c);
120 } else {
121 const QString decomposition = c.decomposition();
122 if (!decomposition.isEmpty() && decomposition[0] < 128)
123 stripped.append(decomposition[0]);
124 else
125 stripped.append("_");
126 }
127 }
128 filename = stripped;
129 }
130
131 // Fix any parts of the path that start with dots.
132 QStringList parts = filename.split("/");
133 for (int i = 0; i < parts.count(); ++i) {
134 QString* part = &parts[i];
135 for (int j = 0; j < kInvalidPrefixCharactersCount; ++j) {
136 if (part->startsWith(kInvalidPrefixCharacters[j])) {
137 part->replace(0, 1, '_');
138 break;
139 }
140 }
141 }
142
143 return parts.join("/");
144 }
145
ParseBlock(QString block,const Song & song,bool * any_empty) const146 QString OrganiseFormat::ParseBlock(QString block, const Song& song,
147 bool* any_empty) const {
148 QRegExp tag_regexp(kTagPattern);
149 QRegExp block_regexp(kBlockPattern);
150
151 // Find any blocks first
152 int pos = 0;
153 while ((pos = block_regexp.indexIn(block, pos)) != -1) {
154 // Recursively parse the block
155 bool empty = false;
156 QString value = ParseBlock(block_regexp.cap(1), song, &empty);
157 if (empty) value = "";
158
159 // Replace the block's value
160 block.replace(pos, block_regexp.matchedLength(), value);
161 pos += value.length();
162 }
163
164 // Now look for tags
165 bool empty = false;
166 pos = 0;
167 while ((pos = tag_regexp.indexIn(block, pos)) != -1) {
168 QString value = TagValue(tag_regexp.cap(1), song);
169 if (value.isEmpty()) empty = true;
170
171 block.replace(pos, tag_regexp.matchedLength(), value);
172 pos += value.length();
173 }
174
175 if (any_empty) *any_empty = empty;
176 return block;
177 }
178
TagValue(const QString & tag,const Song & song) const179 QString OrganiseFormat::TagValue(const QString& tag, const Song& song) const {
180 QString value;
181
182 // TODO(sobkas): What about nice switch statement?
183
184 if (tag == "title")
185 value = song.title();
186 else if (tag == "album")
187 value = song.album();
188 else if (tag == "artist")
189 value = song.artist();
190 else if (tag == "composer")
191 value = song.composer();
192 else if (tag == "performer")
193 value = song.performer();
194 else if (tag == "grouping")
195 value = song.grouping();
196 else if (tag == "lyrics")
197 value = song.lyrics();
198 else if (tag == "genre")
199 value = song.genre();
200 else if (tag == "comment")
201 value = song.comment();
202 else if (tag == "year")
203 value = QString::number(song.year());
204 else if (tag == "originalyear")
205 value = QString::number(song.effective_originalyear());
206 else if (tag == "track")
207 value = QString::number(song.track());
208 else if (tag == "disc")
209 value = QString::number(song.disc());
210 else if (tag == "bpm")
211 value = QString::number(song.bpm());
212 else if (tag == "length")
213 value = QString::number(song.length_nanosec() / kNsecPerSec);
214 else if (tag == "bitrate")
215 value = QString::number(song.bitrate());
216 else if (tag == "samplerate")
217 value = QString::number(song.samplerate());
218 else if (tag == "extension")
219 value = QFileInfo(song.url().toLocalFile()).suffix();
220 else if (tag == "artistinitial") {
221 value = song.effective_albumartist().trimmed();
222 if (replace_the_ && !value.isEmpty())
223 value.replace(QRegExp("^the\\s+", Qt::CaseInsensitive), "");
224 if (!value.isEmpty()) value = value[0].toUpper();
225 } else if (tag == "albumartist") {
226 value = song.is_compilation() ? "Various Artists"
227 : song.effective_albumartist();
228 }
229
230 if (replace_the_ && (tag == "artist" || tag == "albumartist"))
231 value.replace(QRegExp("^the\\s+", Qt::CaseInsensitive), "");
232
233 if (value == "0" || value == "-1") value = "";
234
235 // Prepend a 0 to single-digit track numbers
236 if (tag == "track" && value.length() == 1) value.prepend('0');
237
238 // Replace characters that really shouldn't be in paths
239 for (int i = 0; i < kInvalidFatCharactersCount; ++i) {
240 value.replace(kInvalidFatCharacters[i], '_');
241 }
242
243 return value;
244 }
245
Validator(QObject * parent)246 OrganiseFormat::Validator::Validator(QObject* parent) : QValidator(parent) {}
247
validate(QString & input,int &) const248 QValidator::State OrganiseFormat::Validator::validate(QString& input,
249 int&) const {
250 QRegExp tag_regexp(kTagPattern);
251
252 // Make sure all the blocks match up
253 int block_level = 0;
254 for (int i = 0; i < input.length(); ++i) {
255 if (input[i] == '{')
256 block_level++;
257 else if (input[i] == '}')
258 block_level--;
259
260 if (block_level < 0 || block_level > 1) return QValidator::Invalid;
261 }
262
263 if (block_level != 0) return QValidator::Invalid;
264
265 // Make sure the tags are valid
266 int pos = 0;
267 while ((pos = tag_regexp.indexIn(input, pos)) != -1) {
268 if (!OrganiseFormat::kKnownTags.contains(tag_regexp.cap(1)))
269 return QValidator::Invalid;
270
271 pos += tag_regexp.matchedLength();
272 }
273
274 return QValidator::Acceptable;
275 }
276
SyntaxHighlighter(QObject * parent)277 OrganiseFormat::SyntaxHighlighter::SyntaxHighlighter(QObject* parent)
278 : QSyntaxHighlighter(parent) {}
279
SyntaxHighlighter(QTextEdit * parent)280 OrganiseFormat::SyntaxHighlighter::SyntaxHighlighter(QTextEdit* parent)
281 : QSyntaxHighlighter(parent) {}
282
SyntaxHighlighter(QTextDocument * parent)283 OrganiseFormat::SyntaxHighlighter::SyntaxHighlighter(QTextDocument* parent)
284 : QSyntaxHighlighter(parent) {}
285
highlightBlock(const QString & text)286 void OrganiseFormat::SyntaxHighlighter::highlightBlock(const QString& text) {
287 const bool light =
288 QApplication::palette().color(QPalette::Base).value() > 128;
289 const QRgb block_color = light ? kBlockColorLight : kBlockColorDark;
290 const QRgb valid_tag_color = light ? kValidTagColorLight : kValidTagColorDark;
291 const QRgb invalid_tag_color =
292 light ? kInvalidTagColorLight : kInvalidTagColorDark;
293
294 QRegExp tag_regexp(kTagPattern);
295 QRegExp block_regexp(kBlockPattern);
296
297 QTextCharFormat block_format;
298 block_format.setBackground(QColor(block_color));
299
300 // Reset formatting
301 setFormat(0, text.length(), QTextCharFormat());
302
303 // Blocks
304 int pos = 0;
305 while ((pos = block_regexp.indexIn(text, pos)) != -1) {
306 setFormat(pos, block_regexp.matchedLength(), block_format);
307
308 pos += block_regexp.matchedLength();
309 }
310
311 // Tags
312 pos = 0;
313 while ((pos = tag_regexp.indexIn(text, pos)) != -1) {
314 QTextCharFormat f = format(pos);
315 f.setForeground(
316 QColor(OrganiseFormat::kKnownTags.contains(tag_regexp.cap(1))
317 ? valid_tag_color
318 : invalid_tag_color));
319
320 setFormat(pos, tag_regexp.matchedLength(), f);
321 pos += tag_regexp.matchedLength();
322 }
323 }
324