/* * Cantata * * Copyright (c) 2011-2022 Craig Drummond * * ---- * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "deviceoptions.h" #include "mpd-interface/song.h" #include "support/configuration.h" #include #include #ifndef _MSC_VER #include #endif static QString cleanPath(const QString &path) { /* Unicode uses combining characters to form accented versions of other characters. * (Exception: Latin-1 table for compatibility with ASCII.) * Those can be found in the Unicode tables listed at: * http://en.wikipedia.org/w/index.php?title=Combining_character&oldid=255990982 * Removing those characters removes accents. :) */ QString result = path; // German umlauts result.replace(QChar(0x00e4), "ae").replace(QChar(0x00c4), "Ae"); result.replace(QChar(0x00f6), "oe").replace(QChar(0x00d6), "Oe"); result.replace(QChar(0x00fc), "ue").replace(QChar(0x00dc), "Ue"); result.replace(QChar(0x00df), "ss"); // other special cases result.replace(QChar(0x00C6), "AE"); result.replace(QChar(0x00E6), "ae"); result.replace(QChar(0x00D8), "OE"); result.replace(QChar(0x00F8), "oe"); // normalize in a form where accents are separate characters result = result.normalized(QString::NormalizationForm_D); // remove accents from table "Combining Diacritical Marks" for (int i = 0x0300; i <= 0x036F; i++) { result.remove(QChar(i)); } return result; } static QString asciiPath(const QString &path) { QString result = path; for (int i = 0; i < result.length(); i++) { QChar c = result[ i ]; if (c > QChar(0x7f) || QChar(0)==c) { c = '_'; } result[ i ] = c; } return result; } QString vfatPath(const QString &path) { QString s = path; if ('/'==QDir::separator()) {// we are on *nix, \ is a valid character in file or directory names, NOT the dir separator s.replace('\\', '_'); } else { s.replace('/', '_'); // on windows we have to replace / instead } for (int i = 0; i < s.length(); i++) { QChar c = s[ i ]; if (c < QChar(0x20) || c == QChar(0x7F) // 0x7F = 127 = DEL control character || '*'==c || '?'==c || '<'==c || '>'==c || '|'==c || '"'==c || ':'==c) { c = '_'; } else if ('['==c) { c = '('; } else if (']'==c) { c = ')'; } s[ i ] = c; } /* beware of reserved device names */ uint len = s.length(); if (3==len || (len > 3 && '.'==s[3])) { QString l = s.left(3).toLower(); if ("aux"==l || "con"==l || "nul"==l || "prn"==l) { s = '_' + s; } } else if (4==len || (len > 4 && '.'==s[4])) { QString l = s.left(3).toLower(); QString d = s.mid(3,1); if (("com"==l || "lpt"==l) && ("0"==d || "1"==d || "2"==d || "3"==d || "4"==d || "5"==d || "6"==d || "7"==d || "8"==d || "9"==d)) { s = '_' + s; } } // "clock$" is only allowed WITH extension, according to: // http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations if (0==QString::compare(s, "clock$", Qt::CaseInsensitive)) { s = '_' + s; } /* max path length of Windows API */ s = s.left(255); /* whitespace at the end of folder/file names or extensions are bad */ len = s.length(); if (' '==s[len-1]) { s[len-1] = '_'; } int extensionIndex = s.lastIndexOf('.'); // correct trailing spaces in file name itself if (s.length()>1 && extensionIndex>0 && ' '==s.at(extensionIndex-1)) { s[extensionIndex - 1] = '_'; } for (int i = 1; i < s.length(); i++) {// correct trailing whitespace in folder names if ((s.at(i) == QDir::separator()) && ' '==s.at(i-1)) { s[i - 1] = '_'; } } return s; } static void manipulateThe(QString &str, bool reverse) { if (reverse) { if (!str.startsWith("the ", Qt::CaseInsensitive)) { return; } QString begin = str.left(3); str = str.append(", %1").arg(begin); str = str.mid(4); return; } if(!str.endsWith(", the", Qt::CaseInsensitive)) { return; } QString end = str.right(3); str = str.prepend("%1 ").arg(end); str.truncate(str.length() - end.length() - 2); } const QLatin1String DeviceOptions::constAlbumArtist("%albumartist%"); const QLatin1String DeviceOptions::constComposer("%composer%"); const QLatin1String DeviceOptions::constAlbumTitle("%album%"); const QLatin1String DeviceOptions::constTrackArtist("%artist%"); const QLatin1String DeviceOptions::constTrackTitle("%title%"); const QLatin1String DeviceOptions::constTrackArtistAndTitle("%artistandtitle%"); const QLatin1String DeviceOptions::constTrackNumber("%track%"); const QLatin1String DeviceOptions::constCdNumber("%discnumber%"); const QLatin1String DeviceOptions::constGenre("%genre%"); const QLatin1String DeviceOptions::constYear("%year%"); #ifdef ENABLE_DEVICES_SUPPORT DeviceOptions::DeviceOptions(const QString &cvrName) #else DeviceOptions::DeviceOptions() #endif : scheme(constAlbumArtist+QChar('/')+ constAlbumTitle+QChar('/')+ constTrackNumber+QChar(' ')+ constTrackTitle) , vfatSafe(true) , asciiOnly(false) , ignoreThe(false) , replaceSpaces(false) #ifdef ENABLE_DEVICES_SUPPORT , fixVariousArtists(false) , transcoderValue(0) , transcoderWhen(TW_Always) , useCache(true) , autoScan(false) , coverName(cvrName) , coverMaxSize(0) #endif { } static const QLatin1String constMpdGroup("mpd"); static const QLatin1String constSchemeKey("scheme"); static const QLatin1String constVFatKey("vfatSafe"); static const QLatin1String constAsciiKey("asciiOnly"); static const QLatin1String constIgnoreTheKey("ignoreThe"); static const QLatin1String constSpacesKey("replaceSpaces"); #ifdef ENABLE_DEVICES_SUPPORT static const QLatin1String constTransCodecKey("transcoderCodec"); static const QLatin1String constTransValKey("transcoderValue"); static const QLatin1String constTransIfDiffKey("transcoderWhenDifferent"); static const QLatin1String constTransIfLosslessKey("transcoderWhenSourceIsLosssless"); static const QLatin1String constTransWhenKey("transcoderWhen"); #endif static const QLatin1String constUseCacheKey("useCache"); static const QLatin1String constFixVaKey("fixVariousArtists"); static const QLatin1String constNameKey("name"); static const QLatin1String constCvrFileKey("coverFileName"); static const QLatin1String constCvrMaxKey("coverMaxSize"); static const QLatin1String constVolKey("volumeId"); bool DeviceOptions::isConfigured(const QString &group, bool isMpd) { if (Configuration(group).hasEntry(constSchemeKey)) { return true; } if (isMpd) { return Configuration(constMpdGroup).hasEntry(constSchemeKey); } return false; } void DeviceOptions::load(const QString &group, bool isMpd) { Configuration cfg(group); if (isMpd && !cfg.hasEntry(constSchemeKey)) { // Try old [mpd] group... Configuration mpdGrp(constMpdGroup); if (mpdGrp.hasEntry(constSchemeKey)) { scheme=mpdGrp.get(constSchemeKey, scheme); vfatSafe=mpdGrp.get(constVFatKey, vfatSafe); asciiOnly=mpdGrp.get(constAsciiKey, asciiOnly); ignoreThe=mpdGrp.get(constIgnoreTheKey, ignoreThe); replaceSpaces=mpdGrp.get(constSpacesKey, replaceSpaces); #ifdef ENABLE_DEVICES_SUPPORT transcoderCodec=mpdGrp.get(constTransCodecKey, (isMpd ? "lame" : transcoderCodec)); transcoderValue=mpdGrp.get(constTransValKey, (isMpd ? 2 : transcoderValue)); if (mpdGrp.hasEntry(constTransWhenKey)) { transcoderWhen=(TranscodeWhen)mpdGrp.get(constTransWhenKey, (int)transcoderWhen); } else { if (mpdGrp.get(constTransIfLosslessKey, false)) { transcoderWhen=TW_IfLossess; } else if (mpdGrp.get(constTransIfDiffKey, false)) { transcoderWhen=TW_IfDifferent; } else { transcoderWhen=TW_Always; } } #endif // Save these settings into new group, [mpd] is left for other connections... save(group, true, true, true); } } else { scheme=cfg.get(constSchemeKey, scheme); vfatSafe=cfg.get(constVFatKey, vfatSafe); asciiOnly=cfg.get(constAsciiKey, asciiOnly); ignoreThe=cfg.get(constIgnoreTheKey, ignoreThe); replaceSpaces=cfg.get(constSpacesKey, replaceSpaces); #ifdef ENABLE_DEVICES_SUPPORT if (!isMpd) { useCache=cfg.get(constUseCacheKey, useCache); fixVariousArtists=cfg.get(constFixVaKey, fixVariousArtists); name=cfg.get(constNameKey, name); coverName=cfg.get(constCvrFileKey, coverName); coverMaxSize=cfg.get(constCvrMaxKey, coverMaxSize); volumeId=cfg.get(constVolKey, volumeId); checkCoverSize(); } transcoderCodec=cfg.get(constTransCodecKey, (isMpd ? "lame" : transcoderCodec)); transcoderValue=cfg.get(constTransValKey, (isMpd ? 2 : transcoderValue)); if (cfg.hasEntry(constTransWhenKey)) { transcoderWhen=(TranscodeWhen)cfg.get(constTransWhenKey, (int)transcoderWhen); } else { if (cfg.get(constTransIfLosslessKey, false)) { transcoderWhen=TW_IfLossess; } else if (cfg.get(constTransIfDiffKey, false)) { transcoderWhen=TW_IfDifferent; } else { transcoderWhen=TW_Always; } } #endif } } void DeviceOptions::save(const QString &group, bool isMpd, bool saveTrans, bool saveFileName) const { Configuration cfg(group); if (saveFileName) { cfg.set(constSchemeKey, scheme); cfg.set(constVFatKey, vfatSafe); cfg.set(constAsciiKey, asciiOnly); cfg.set(constIgnoreTheKey, ignoreThe); cfg.set(constSpacesKey, replaceSpaces); } #ifdef ENABLE_DEVICES_SUPPORT if (!isMpd) { cfg.set(constUseCacheKey, useCache); cfg.set(constFixVaKey, fixVariousArtists); cfg.set(constNameKey, name); cfg.set(constCvrFileKey, coverName); cfg.set(constCvrMaxKey, coverMaxSize); cfg.set(constVolKey, volumeId); } if (saveTrans) { cfg.set(constTransCodecKey, transcoderCodec); cfg.set(constTransValKey, transcoderValue); cfg.set(constTransWhenKey, (int)transcoderWhen); } #else Q_UNUSED(isMpd) Q_UNUSED(saveTrans) #endif } QString DeviceOptions::clean(const QString &str) const { QString result(str); if (asciiOnly) { result = cleanPath(result); result = asciiPath(result); } result=result.simplified(); if (replaceSpaces) { result.replace(QRegExp("\\s"), "_"); } if (vfatSafe) { result = vfatPath(result); } result.replace('/', '-'); return result; } Song DeviceOptions::clean(const Song &s) const { Song copy=s; if (ignoreThe) { manipulateThe(copy.albumartist, true); manipulateThe(copy.artist, true); } copy.albumartist=clean(copy.albumartist); copy.artist=clean(copy.artist); copy.album=clean(copy.album); copy.title=clean(copy.title); for (int i=0; i