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