File indexing completed on 2024-05-19 04:49:31
0001 /**************************************************************************************** 0002 * Copyright (c) 2002 Mark Kretschmann <kretschmann@kde.org> * 0003 * * 0004 * This program is free software; you can redistribute it and/or modify it under * 0005 * the terms of the GNU General Public License as published by the Free Software * 0006 * Foundation; either version 2 of the License, or (at your option) any later * 0007 * version. * 0008 * * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0012 * * 0013 * You should have received a copy of the GNU General Public License along with * 0014 * this program. If not, see <http://www.gnu.org/licenses/>. * 0015 ****************************************************************************************/ 0016 0017 #include "core/support/Amarok.h" 0018 0019 #include "core/meta/Meta.h" 0020 #include "core/meta/support/MetaUtility.h" 0021 #include "core/capabilities/SourceInfoCapability.h" 0022 #include "core/playlists/PlaylistFormat.h" 0023 0024 #include <KConfigGroup> 0025 #include <KLocalizedString> 0026 #include <KSharedConfig> 0027 0028 #include <QApplication> 0029 #include <QDateTime> 0030 #include <QIcon> 0031 #include <QLocale> 0032 #include <QPixmapCache> 0033 #include <QStandardPaths> 0034 0035 QPointer<KActionCollection> Amarok::actionCollectionObject; 0036 QMutex Amarok::globalDirsMutex; 0037 0038 namespace Amarok 0039 { 0040 0041 // TODO: sometimes we have a playcount but no valid datetime. 0042 // in such a case we should maybe display "Unknown" and not "Never" 0043 QString verboseTimeSince( const QDateTime &datetime ) 0044 { 0045 if( datetime.isNull() || !datetime.toSecsSinceEpoch() ) 0046 return i18nc( "The amount of time since last played", "Never" ); 0047 0048 const QDateTime now = QDateTime::currentDateTime(); 0049 const int datediff = datetime.daysTo( now ); 0050 0051 // HACK: Fix 203522. Arithmetic overflow? 0052 // Getting weird values from Plasma::DataEngine (LAST_PLAYED field). 0053 if( datediff < 0 ) 0054 return i18nc( "When this track was last played", "Unknown" ); 0055 0056 if( datediff >= 6*7 /*six weeks*/ ) { // return absolute month/year 0057 QString month_year = datetime.date().toString(QStringLiteral("MM yyyy")); 0058 return i18nc( "monthname year", "%1", month_year ); 0059 } 0060 0061 //TODO "last week" = maybe within 7 days, but prolly before last Sunday 0062 0063 if( datediff >= 7 ) // return difference in weeks 0064 return i18np( "One week ago", "%1 weeks ago", (datediff+3)/7 ); 0065 0066 const int timediff = datetime.secsTo( now ); 0067 0068 if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days 0069 return datediff == 1 ? 0070 i18n( "Yesterday" ) : 0071 i18np( "One day ago", "%1 days ago", (timediff+12*60*60)/(24*60*60) ); 0072 0073 if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours 0074 return i18np( "One hour ago", "%1 hours ago", (timediff+30*60)/(60*60) ); 0075 0076 //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" 0077 0078 if( timediff >= 0 ) // return difference in minutes 0079 return timediff/60 ? 0080 i18np( "One minute ago", "%1 minutes ago", (timediff+30)/60 ) : 0081 i18n( "Within the last minute" ); 0082 0083 return i18n( "The future" ); 0084 } 0085 0086 QString verboseTimeSince( uint time_t ) 0087 { 0088 if( !time_t ) 0089 return i18nc( "The amount of time since last played", "Never" ); 0090 0091 QDateTime dt; 0092 dt.setSecsSinceEpoch( time_t ); 0093 return verboseTimeSince( dt ); 0094 } 0095 0096 QString conciseTimeSince( uint time_t ) 0097 { 0098 if( !time_t ) 0099 return i18nc( "The amount of time since last played", "0" ); 0100 0101 QDateTime datetime; 0102 datetime.setSecsSinceEpoch( time_t ); 0103 0104 const QDateTime now = QDateTime::currentDateTime(); 0105 const int datediff = datetime.daysTo( now ); 0106 0107 if( datediff >= 6*7 /*six weeks*/ ) { // return difference in months 0108 return i18nc( "number of months ago", "%1M", datediff/7/4 ); 0109 } 0110 0111 if( datediff >= 7 ) // return difference in weeks 0112 return i18nc( "w for weeks", "%1w", (datediff+3)/7 ); 0113 0114 if( datediff == -1 ) 0115 return i18nc( "When this track was last played", "Tomorrow" ); 0116 0117 const int timediff = datetime.secsTo( now ); 0118 0119 if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days 0120 // xgettext: no-c-format 0121 return i18nc( "d for days", "%1d", (timediff+12*60*60)/(24*60*60) ); 0122 0123 if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours 0124 return i18nc( "h for hours", "%1h", (timediff+30*60)/(60*60) ); 0125 0126 //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" 0127 0128 if( timediff >= 60 ) // return difference in minutes 0129 return QStringLiteral("%1'").arg( ( timediff + 30 )/60 ); 0130 if( timediff >= 0 ) // return difference in seconds 0131 return QStringLiteral("%1\"").arg( ( timediff + 1 )/60 ); 0132 0133 return i18n( "0" ); 0134 } 0135 0136 void manipulateThe( QString &str, bool reverse ) 0137 { 0138 if( reverse ) 0139 { 0140 if( !str.startsWith( QLatin1String("the "), Qt::CaseInsensitive ) ) 0141 return; 0142 0143 QString begin = str.left( 3 ); 0144 str = str.append( ", %1" ).arg( begin ); 0145 str = str.mid( 4 ); 0146 return; 0147 } 0148 0149 if( !str.endsWith( QLatin1String(", the"), Qt::CaseInsensitive ) ) 0150 return; 0151 0152 QString end = str.right( 3 ); 0153 str = str.prepend( "%1 " ).arg( end ); 0154 0155 uint newLen = str.length() - end.length() - 2; 0156 0157 str.truncate( newLen ); 0158 } 0159 0160 QString generatePlaylistName( const Meta::TrackList& tracks ) 0161 { 0162 QString datePart = QLocale::system().toString( QDateTime::currentDateTime(), 0163 QLocale::ShortFormat ); 0164 if( tracks.isEmpty() ) 0165 { 0166 return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ 0167 the parentheses", 0168 "Empty Playlist (%1)", datePart ); 0169 } 0170 0171 bool singleArtist = true; 0172 bool singleAlbum = true; 0173 0174 Meta::ArtistPtr artist = tracks.first()->artist(); 0175 Meta::AlbumPtr album = tracks.first()->album(); 0176 0177 QString artistPart; 0178 QString albumPart; 0179 0180 foreach( const Meta::TrackPtr track, tracks ) 0181 { 0182 if( artist != track->artist() ) 0183 singleArtist = false; 0184 0185 if( album != track->album() ) 0186 singleAlbum = false; 0187 0188 if ( !singleArtist && !singleAlbum ) 0189 break; 0190 } 0191 0192 if( ( !singleArtist && !singleAlbum ) || 0193 ( !artist && !album ) ) 0194 return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ 0195 the parentheses", 0196 "Various Tracks (%1)", datePart ); 0197 0198 if( singleArtist ) 0199 { 0200 if( artist ) 0201 artistPart = artist->prettyName(); 0202 else 0203 artistPart = i18n( "Unknown Artist(s)" ); 0204 } 0205 else if( album && album->hasAlbumArtist() && singleAlbum ) 0206 { 0207 artistPart = album->albumArtist()->prettyName(); 0208 } 0209 else 0210 { 0211 artistPart = i18n( "Various Artists" ); 0212 } 0213 0214 if( singleAlbum ) 0215 { 0216 if( album ) 0217 albumPart = album->prettyName(); 0218 else 0219 albumPart = i18n( "Unknown Album(s)" ); 0220 } 0221 else 0222 { 0223 albumPart = i18n( "Various Albums" ); 0224 } 0225 0226 return i18nc( "A saved playlist titled <artist> - <album>", "%1 - %2", 0227 artistPart, albumPart ); 0228 } 0229 0230 KActionCollection* actionCollection() // TODO: constify? 0231 { 0232 if( !actionCollectionObject ) 0233 { 0234 actionCollectionObject = new KActionCollection( qApp ); 0235 actionCollectionObject->setObjectName( QStringLiteral("Amarok-KActionCollection") ); 0236 } 0237 0238 return actionCollectionObject.data(); 0239 } 0240 0241 KConfigGroup config( const QString &group ) 0242 { 0243 //Slightly more useful config() that allows setting the group simultaneously 0244 return KSharedConfig::openConfig()->group( group ); 0245 } 0246 0247 namespace ColorScheme 0248 { 0249 QColor Base; 0250 QColor Text; 0251 QColor Background; 0252 QColor Foreground; 0253 QColor AltBase; 0254 } 0255 0256 OverrideCursor::OverrideCursor( Qt::CursorShape cursor ) 0257 { 0258 QApplication::setOverrideCursor( cursor == Qt::WaitCursor ? 0259 Qt::WaitCursor : 0260 Qt::BusyCursor ); 0261 } 0262 0263 OverrideCursor::~OverrideCursor() 0264 { 0265 QApplication::restoreOverrideCursor(); 0266 } 0267 0268 QString saveLocation( const QString &directory ) 0269 { 0270 globalDirsMutex.lock(); 0271 QString result = QStandardPaths::writableLocation( QStandardPaths::AppDataLocation ) + QDir::separator() + directory; 0272 0273 if( !result.endsWith( QDir::separator() ) ) 0274 result.append( QDir::separator() ); 0275 0276 QDir dir( result ); 0277 if( !dir.exists() ) 0278 dir.mkpath( QStringLiteral( "." ) ); 0279 0280 globalDirsMutex.unlock(); 0281 return result; 0282 } 0283 0284 QString defaultPlaylistPath() 0285 { 0286 return Amarok::saveLocation() + QLatin1String("current.xspf"); 0287 } 0288 0289 QString cleanPath( const QString &path ) 0290 { 0291 /* Unicode uses combining characters to form accented versions of other characters. 0292 * (Exception: Latin-1 table for compatibility with ASCII.) 0293 * Those can be found in the Unicode tables listed at: 0294 * http://en.wikipedia.org/w/index.php?title=Combining_character&oldid=255990982 0295 * Removing those characters removes accents. :) */ 0296 QString result = path; 0297 0298 // German umlauts 0299 result.replace( QChar(0x00e4), QLatin1String("ae") ).replace( QChar(0x00c4), QLatin1String("Ae") ); 0300 result.replace( QChar(0x00f6), QLatin1String("oe") ).replace( QChar(0x00d6), QLatin1String("Oe") ); 0301 result.replace( QChar(0x00fc), QLatin1String("ue") ).replace( QChar(0x00dc), QLatin1String("Ue") ); 0302 result.replace( QChar(0x00df), QLatin1String("ss") ); 0303 0304 // other special cases 0305 result.replace( QChar(0x00C6), QLatin1String("AE") ); 0306 result.replace( QChar(0x00E6), QLatin1String("ae") ); 0307 0308 result.replace( QChar(0x00D8), QLatin1String("OE") ); 0309 result.replace( QChar(0x00F8), QLatin1String("oe") ); 0310 0311 // normalize in a form where accents are separate characters 0312 result = result.normalized( QString::NormalizationForm_D ); 0313 0314 // remove accents from table "Combining Diacritical Marks" 0315 for( int i = 0x0300; i <= 0x036F; i++ ) 0316 { 0317 result.remove( QChar( i ) ); 0318 } 0319 0320 return result; 0321 } 0322 0323 QString asciiPath( const QString &path ) 0324 { 0325 QString result = path; 0326 for( int i = 0; i < result.length(); i++ ) 0327 { 0328 QChar c = result[ i ]; 0329 if( c > QChar(0x7f) || c == QChar(0) ) 0330 { 0331 c = '_'; 0332 } 0333 result[ i ] = c; 0334 } 0335 return result; 0336 } 0337 0338 QString vfatPath( const QString &path, PathSeparatorBehaviour behaviour ) 0339 { 0340 if( path.isEmpty() ) 0341 return QString(); 0342 0343 QString s = path; 0344 0345 QChar separator = ( behaviour == AutoBehaviour ) ? QDir::separator() : ( behaviour == UnixBehaviour ) ? '/' : '\\'; 0346 0347 if( behaviour == UnixBehaviour ) // we are on *nix, \ is a valid character in file or directory names, NOT the dir separator 0348 s.replace( '\\', '_' ); 0349 else 0350 s.replace( QLatin1Char('/'), '_' ); // on windows we have to replace / instead 0351 0352 int start = 0; 0353 #ifdef Q_OS_WIN 0354 // exclude the leading "C:/" from special character replacement in the loop below 0355 // bug 279560, bug 302251 0356 if( QDir::isAbsolutePath( s ) ) 0357 start = 3; 0358 #endif 0359 for( int i = start; i < s.length(); i++ ) 0360 { 0361 QChar c = s[ i ]; 0362 if( c < QChar(0x20) || c == QChar(0x7F) // 0x7F = 127 = DEL control character 0363 || c=='*' || c=='?' || c=='<' || c=='>' 0364 || c=='|' || c=='"' || c==':' ) 0365 c = '_'; 0366 else if( c == '[' ) 0367 c = '('; 0368 else if ( c == ']' ) 0369 c = ')'; 0370 s[ i ] = c; 0371 } 0372 0373 /* beware of reserved device names */ 0374 uint len = s.length(); 0375 if( len == 3 || (len > 3 && s[3] == '.') ) 0376 { 0377 QString l = s.left(3).toLower(); 0378 if( l==QLatin1String("aux") || l==QLatin1String("con") || l==QLatin1String("nul") || l==QLatin1String("prn") ) 0379 s = '_' + s; 0380 } 0381 else if( len == 4 || (len > 4 && s[4] == '.') ) 0382 { 0383 QString l = s.left(3).toLower(); 0384 QString d = s.mid(3,1); 0385 if( (l==QLatin1String("com") || l==QLatin1String("lpt")) && 0386 (d==QLatin1String("0") || d==QLatin1String("1") || d==QLatin1String("2") || d==QLatin1String("3") || d==QLatin1String("4") || 0387 d==QLatin1String("5") || d==QLatin1String("6") || d==QLatin1String("7") || d==QLatin1String("8") || d==QLatin1String("9")) ) 0388 s = '_' + s; 0389 } 0390 0391 // "clock$" is only allowed WITH extension, according to: 0392 // http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations 0393 if( QString::compare( s, QStringLiteral("clock$"), Qt::CaseInsensitive ) == 0 ) 0394 s = '_' + s; 0395 0396 /* max path length of Windows API */ 0397 s = s.left(255); 0398 0399 /* whitespace or dot at the end of folder/file names or extensions are bad */ 0400 len = s.length(); 0401 if( s.at(len - 1) == ' ' || s.at(len - 1) == '.' ) 0402 s[len - 1] = '_'; 0403 0404 for( int i = 1; i < s.length(); i++ ) // correct trailing whitespace in folder names 0405 { 0406 if( s.at(i) == separator && s.at(i - 1) == ' ' ) 0407 s[i - 1] = '_'; 0408 } 0409 0410 for( int i = 1; i < s.length(); i++ ) // correct trailing dot in folder names, excluding . and .. 0411 { 0412 if( s.at(i) == separator 0413 && s.at(i - 1) == '.' 0414 && !( i == 1 // ./any 0415 || ( i == 2 && s.at(i - 2) == '.' ) // ../any 0416 || ( i >= 2 && s.at(i - 2) == separator ) // any/./any 0417 || ( i >= 3 && s.at(i - 3) == separator && s.at(i - 2) == '.' ) // any/../any 0418 ) ) 0419 s[i - 1] = '_'; 0420 } 0421 0422 /* correct trailing spaces in file name itself, not needed for dots */ 0423 int extensionIndex = s.lastIndexOf( QLatin1Char('.') ); 0424 if( ( s.length() > 1 ) && ( extensionIndex > 0 ) ) 0425 if( s.at(extensionIndex - 1) == ' ' ) 0426 s[extensionIndex - 1] = '_'; 0427 0428 return s; 0429 } 0430 0431 QPixmap semiTransparentLogo( int dim ) 0432 { 0433 QPixmap logo; 0434 #define AMAROK_LOGO_CACHE_KEY QLatin1String("AmarokSemiTransparentLogo")+QString::number(dim) 0435 if( !QPixmapCache::find( AMAROK_LOGO_CACHE_KEY, &logo ) ) 0436 { 0437 QImage amarokIcon = QIcon::fromTheme( QStringLiteral("amarok") ).pixmap( dim, dim ).toImage(); 0438 amarokIcon = amarokIcon.convertToFormat( QImage::Format_ARGB32 ); 0439 QRgb *data = reinterpret_cast<QRgb*>( amarokIcon.bits() ); 0440 QRgb *end = data + amarokIcon.sizeInBytes() / 4; 0441 while(data != end) 0442 { 0443 unsigned char gray = qGray(*data); 0444 *data = qRgba(gray, gray, gray, 127); 0445 ++data; 0446 } 0447 logo = QPixmap::fromImage( amarokIcon ); 0448 QPixmapCache::insert( AMAROK_LOGO_CACHE_KEY, logo ); 0449 } 0450 #undef AMAROK_LOGO_CACHE_KEY 0451 return logo; 0452 } 0453 0454 } // End namespace Amarok