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