1 /****************************************************************************************
2  * Copyright (c) 2002 Mark Kretschmann <kretschmann@kde.org>                            *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "core/support/Amarok.h"
18 
19 #include "core/meta/Meta.h"
20 #include "core/meta/support/MetaUtility.h"
21 #include "core/capabilities/SourceInfoCapability.h"
22 #include "core/playlists/PlaylistFormat.h"
23 
24 #include <KConfigGroup>
25 #include <KLocalizedString>
26 #include <KSharedConfig>
27 
28 #include <QApplication>
29 #include <QDateTime>
30 #include <QIcon>
31 #include <QLocale>
32 #include <QPixmapCache>
33 #include <QStandardPaths>
34 
35 QPointer<KActionCollection> Amarok::actionCollectionObject;
36 QMutex Amarok::globalDirsMutex;
37 
38 namespace Amarok
39 {
40 
41     // TODO: sometimes we have a playcount but no valid datetime.
42     //   in such a case we should maybe display "Unknown" and not "Never"
verboseTimeSince(const QDateTime & datetime)43     QString verboseTimeSince( const QDateTime &datetime )
44     {
45         if( datetime.isNull() || !datetime.toSecsSinceEpoch() )
46             return i18nc( "The amount of time since last played", "Never" );
47 
48         const QDateTime now = QDateTime::currentDateTime();
49         const int datediff = datetime.daysTo( now );
50 
51         // HACK: Fix 203522. Arithmetic overflow?
52         // Getting weird values from Plasma::DataEngine (LAST_PLAYED field).
53         if( datediff < 0 )
54             return i18nc( "When this track was last played", "Unknown" );
55 
56         if( datediff >= 6*7 /*six weeks*/ ) {  // return absolute month/year
57             QString month_year = datetime.date().toString(QStringLiteral("MM yyyy"));
58             return i18nc( "monthname year", "%1", month_year );
59         }
60 
61         //TODO "last week" = maybe within 7 days, but prolly before last Sunday
62 
63         if( datediff >= 7 )  // return difference in weeks
64             return i18np( "One week ago", "%1 weeks ago", (datediff+3)/7 );
65 
66         const int timediff = datetime.secsTo( now );
67 
68         if( timediff >= 24*60*60 /*24 hours*/ )  // return difference in days
69             return datediff == 1 ?
70                     i18n( "Yesterday" ) :
71                     i18np( "One day ago", "%1 days ago", (timediff+12*60*60)/(24*60*60) );
72 
73         if( timediff >= 90*60 /*90 minutes*/ )  // return difference in hours
74             return i18np( "One hour ago", "%1 hours ago", (timediff+30*60)/(60*60) );
75 
76         //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently"
77 
78         if( timediff >= 0 )  // return difference in minutes
79             return timediff/60 ?
80                     i18np( "One minute ago", "%1 minutes ago", (timediff+30)/60 ) :
81                     i18n( "Within the last minute" );
82 
83         return i18n( "The future" );
84     }
85 
verboseTimeSince(uint time_t)86     QString verboseTimeSince( uint time_t )
87     {
88         if( !time_t )
89             return i18nc( "The amount of time since last played", "Never" );
90 
91         QDateTime dt;
92         dt.setSecsSinceEpoch( time_t );
93         return verboseTimeSince( dt );
94     }
95 
conciseTimeSince(uint time_t)96     QString conciseTimeSince( uint time_t )
97     {
98         if( !time_t )
99             return i18nc( "The amount of time since last played", "0" );
100 
101         QDateTime datetime;
102         datetime.setSecsSinceEpoch( time_t );
103 
104         const QDateTime now = QDateTime::currentDateTime();
105         const int datediff = datetime.daysTo( now );
106 
107         if( datediff >= 6*7 /*six weeks*/ ) {  // return difference in months
108             return i18nc( "number of months ago", "%1M", datediff/7/4 );
109         }
110 
111         if( datediff >= 7 )  // return difference in weeks
112             return i18nc( "w for weeks", "%1w", (datediff+3)/7 );
113 
114         if( datediff == -1 )
115             return i18nc( "When this track was last played", "Tomorrow" );
116 
117         const int timediff = datetime.secsTo( now );
118 
119         if( timediff >= 24*60*60 /*24 hours*/ )  // return difference in days
120             // xgettext: no-c-format
121             return i18nc( "d for days", "%1d", (timediff+12*60*60)/(24*60*60) );
122 
123         if( timediff >= 90*60 /*90 minutes*/ )  // return difference in hours
124             return i18nc( "h for hours", "%1h", (timediff+30*60)/(60*60) );
125 
126         //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently"
127 
128         if( timediff >= 60 )  // return difference in minutes
129             return QStringLiteral("%1'").arg( ( timediff + 30 )/60 );
130         if( timediff >= 0 )  // return difference in seconds
131             return QStringLiteral("%1\"").arg( ( timediff + 1 )/60 );
132 
133         return i18n( "0" );
134     }
135 
manipulateThe(QString & str,bool reverse)136     void manipulateThe( QString &str, bool reverse )
137     {
138         if( reverse )
139         {
140             if( !str.startsWith( QLatin1String("the "), Qt::CaseInsensitive ) )
141                 return;
142 
143             QString begin = str.left( 3 );
144             str = str.append( ", %1" ).arg( begin );
145             str = str.mid( 4 );
146             return;
147         }
148 
149         if( !str.endsWith( QLatin1String(", the"), Qt::CaseInsensitive ) )
150             return;
151 
152         QString end = str.right( 3 );
153         str = str.prepend( "%1 " ).arg( end );
154 
155         uint newLen = str.length() - end.length() - 2;
156 
157         str.truncate( newLen );
158     }
159 
generatePlaylistName(const Meta::TrackList & tracks)160     QString generatePlaylistName( const Meta::TrackList& tracks )
161     {
162         QString datePart = QLocale::system().toString( QDateTime::currentDateTime(),
163                                                        QLocale::ShortFormat );
164         if( tracks.isEmpty() )
165         {
166             return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \
167                           the parentheses",
168                           "Empty Playlist (%1)", datePart );
169         }
170 
171         bool singleArtist = true;
172         bool singleAlbum = true;
173 
174         Meta::ArtistPtr artist = tracks.first()->artist();
175         Meta::AlbumPtr album = tracks.first()->album();
176 
177         QString artistPart;
178         QString albumPart;
179 
180         foreach( const Meta::TrackPtr track, tracks )
181         {
182             if( artist != track->artist() )
183                 singleArtist = false;
184 
185             if( album != track->album() )
186                 singleAlbum = false;
187 
188             if ( !singleArtist && !singleAlbum )
189                 break;
190         }
191 
192         if( ( !singleArtist && !singleAlbum ) ||
193             ( !artist && !album ) )
194             return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \
195                           the parentheses",
196                           "Various Tracks (%1)", datePart );
197 
198         if( singleArtist )
199         {
200             if( artist )
201                 artistPart = artist->prettyName();
202             else
203                 artistPart = i18n( "Unknown Artist(s)" );
204         }
205         else if( album && album->hasAlbumArtist() && singleAlbum )
206         {
207             artistPart = album->albumArtist()->prettyName();
208         }
209         else
210         {
211             artistPart = i18n( "Various Artists" );
212         }
213 
214         if( singleAlbum )
215         {
216             if( album )
217                 albumPart = album->prettyName();
218             else
219                 albumPart = i18n( "Unknown Album(s)" );
220         }
221         else
222         {
223             albumPart = i18n( "Various Albums" );
224         }
225 
226         return i18nc( "A saved playlist titled <artist> - <album>", "%1 - %2",
227                       artistPart, albumPart );
228     }
229 
actionCollection()230     KActionCollection* actionCollection()  // TODO: constify?
231     {
232         if( !actionCollectionObject )
233         {
234             actionCollectionObject = new KActionCollection( qApp );
235             actionCollectionObject->setObjectName( QStringLiteral("Amarok-KActionCollection") );
236         }
237 
238         return actionCollectionObject.data();
239     }
240 
config(const QString & group)241     KConfigGroup config( const QString &group )
242     {
243         //Slightly more useful config() that allows setting the group simultaneously
244         return KSharedConfig::openConfig()->group( group );
245     }
246 
247     namespace ColorScheme
248     {
249         QColor Base;
250         QColor Text;
251         QColor Background;
252         QColor Foreground;
253         QColor AltBase;
254     }
255 
OverrideCursor(Qt::CursorShape cursor)256     OverrideCursor::OverrideCursor( Qt::CursorShape cursor )
257     {
258         QApplication::setOverrideCursor( cursor == Qt::WaitCursor ?
259                                         Qt::WaitCursor :
260                                         Qt::BusyCursor );
261     }
262 
~OverrideCursor()263     OverrideCursor::~OverrideCursor()
264     {
265         QApplication::restoreOverrideCursor();
266     }
267 
saveLocation(const QString & directory)268     QString saveLocation( const QString &directory )
269     {
270         globalDirsMutex.lock();
271         QString result = QStandardPaths::writableLocation( QStandardPaths::AppDataLocation ) + QDir::separator() + directory;
272 
273         if( !result.endsWith( QDir::separator() ) )
274             result.append( QDir::separator() );
275 
276         QDir dir( result );
277         if( !dir.exists() )
278             dir.mkpath( QStringLiteral( "." ) );
279 
280         globalDirsMutex.unlock();
281         return result;
282     }
283 
defaultPlaylistPath()284     QString defaultPlaylistPath()
285     {
286         return Amarok::saveLocation() + QLatin1String("current.xspf");
287     }
288 
cleanPath(const QString & path)289     QString cleanPath( const QString &path )
290     {
291         /* Unicode uses combining characters to form accented versions of other characters.
292          * (Exception: Latin-1 table for compatibility with ASCII.)
293          * Those can be found in the Unicode tables listed at:
294          * http://en.wikipedia.org/w/index.php?title=Combining_character&oldid=255990982
295          * Removing those characters removes accents. :)                                   */
296         QString result = path;
297 
298         // German umlauts
299         result.replace( QChar(0x00e4), QLatin1String("ae") ).replace( QChar(0x00c4), QLatin1String("Ae") );
300         result.replace( QChar(0x00f6), QLatin1String("oe") ).replace( QChar(0x00d6), QLatin1String("Oe") );
301         result.replace( QChar(0x00fc), QLatin1String("ue") ).replace( QChar(0x00dc), QLatin1String("Ue") );
302         result.replace( QChar(0x00df), QLatin1String("ss") );
303 
304         // other special cases
305         result.replace( QChar(0x00C6), QLatin1String("AE") );
306         result.replace( QChar(0x00E6), QLatin1String("ae") );
307 
308         result.replace( QChar(0x00D8), QLatin1String("OE") );
309         result.replace( QChar(0x00F8), QLatin1String("oe") );
310 
311         // normalize in a form where accents are separate characters
312         result = result.normalized( QString::NormalizationForm_D );
313 
314         // remove accents from table "Combining Diacritical Marks"
315         for( int i = 0x0300; i <= 0x036F; i++ )
316         {
317             result.remove( QChar( i ) );
318         }
319 
320         return result;
321     }
322 
asciiPath(const QString & path)323     QString asciiPath( const QString &path )
324     {
325         QString result = path;
326         for( int i = 0; i < result.length(); i++ )
327         {
328             QChar c = result[ i ];
329             if( c > QChar(0x7f) || c == QChar(0) )
330             {
331                 c = '_';
332             }
333             result[ i ] = c;
334         }
335         return result;
336     }
337 
vfatPath(const QString & path,PathSeparatorBehaviour behaviour)338     QString vfatPath( const QString &path, PathSeparatorBehaviour behaviour )
339     {
340         if( path.isEmpty() )
341             return QString();
342 
343         QString s = path;
344 
345         QChar separator = ( behaviour == AutoBehaviour ) ? QDir::separator() : ( behaviour == UnixBehaviour ) ? '/' : '\\';
346 
347         if( behaviour == UnixBehaviour ) // we are on *nix, \ is a valid character in file or directory names, NOT the dir separator
348             s.replace( '\\', '_' );
349         else
350             s.replace( QLatin1Char('/'), '_' ); // on windows we have to replace / instead
351 
352         int start = 0;
353 #ifdef Q_OS_WIN
354         // exclude the leading "C:/" from special character replacement in the loop below
355         // bug 279560, bug 302251
356         if( QDir::isAbsolutePath( s ) )
357             start = 3;
358 #endif
359         for( int i = start; i < s.length(); i++ )
360         {
361             QChar c = s[ i ];
362             if( c < QChar(0x20) || c == QChar(0x7F) // 0x7F = 127 = DEL control character
363                 || c=='*' || c=='?' || c=='<' || c=='>'
364                 || c=='|' || c=='"' || c==':' )
365                 c = '_';
366             else if( c == '[' )
367                 c = '(';
368             else if ( c == ']' )
369                 c = ')';
370             s[ i ] = c;
371         }
372 
373         /* beware of reserved device names */
374         uint len = s.length();
375         if( len == 3 || (len > 3 && s[3] == '.') )
376         {
377             QString l = s.left(3).toLower();
378             if( l==QLatin1String("aux") || l==QLatin1String("con") || l==QLatin1String("nul") || l==QLatin1String("prn") )
379                 s = '_' + s;
380         }
381         else if( len == 4 || (len > 4 && s[4] == '.') )
382         {
383             QString l = s.left(3).toLower();
384             QString d = s.mid(3,1);
385             if( (l==QLatin1String("com") || l==QLatin1String("lpt")) &&
386                     (d==QLatin1String("0") || d==QLatin1String("1") || d==QLatin1String("2") || d==QLatin1String("3") || d==QLatin1String("4") ||
387                      d==QLatin1String("5") || d==QLatin1String("6") || d==QLatin1String("7") || d==QLatin1String("8") || d==QLatin1String("9")) )
388                 s = '_' + s;
389         }
390 
391         // "clock$" is only allowed WITH extension, according to:
392         // http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations
393         if( QString::compare( s, QStringLiteral("clock$"), Qt::CaseInsensitive ) == 0 )
394             s = '_' + s;
395 
396         /* max path length of Windows API */
397         s = s.left(255);
398 
399         /* whitespace or dot at the end of folder/file names or extensions are bad */
400         len = s.length();
401         if( s.at(len - 1) == ' ' || s.at(len - 1) == '.' )
402             s[len - 1] = '_';
403 
404         for( int i = 1; i < s.length(); i++ ) // correct trailing whitespace in folder names
405         {
406             if( s.at(i) == separator && s.at(i - 1) == ' ' )
407                 s[i - 1] = '_';
408         }
409 
410         for( int i = 1; i < s.length(); i++ ) // correct trailing dot in folder names, excluding . and ..
411         {
412             if( s.at(i) == separator
413                     && s.at(i - 1) == '.'
414                     && !( i == 1 // ./any
415                         || ( i == 2 && s.at(i - 2) == '.' ) // ../any
416                         || ( i >= 2 && s.at(i - 2) == separator ) // any/./any
417                         || ( i >= 3 && s.at(i - 3) == separator && s.at(i - 2) == '.' ) // any/../any
418                     ) )
419                 s[i - 1] = '_';
420         }
421 
422         /* correct trailing spaces in file name itself, not needed for dots */
423         int extensionIndex = s.lastIndexOf( QLatin1Char('.') );
424         if( ( s.length() > 1 ) &&  ( extensionIndex > 0 ) )
425             if( s.at(extensionIndex - 1) == ' ' )
426                 s[extensionIndex - 1] = '_';
427 
428         return s;
429     }
430 
semiTransparentLogo(int dim)431     QPixmap semiTransparentLogo( int dim )
432     {
433         QPixmap logo;
434         #define AMAROK_LOGO_CACHE_KEY QLatin1String("AmarokSemiTransparentLogo")+QString::number(dim)
435         if( !QPixmapCache::find( AMAROK_LOGO_CACHE_KEY, &logo ) )
436         {
437             QImage amarokIcon = QIcon::fromTheme( QStringLiteral("amarok") ).pixmap( dim, dim ).toImage();
438             amarokIcon = amarokIcon.convertToFormat( QImage::Format_ARGB32 );
439             QRgb *data = reinterpret_cast<QRgb*>( amarokIcon.bits() );
440             QRgb *end = data + amarokIcon.byteCount() / 4;
441             while(data != end)
442             {
443                 unsigned char gray = qGray(*data);
444                 *data = qRgba(gray, gray, gray, 127);
445                 ++data;
446             }
447             logo = QPixmap::fromImage( amarokIcon );
448             QPixmapCache::insert( AMAROK_LOGO_CACHE_KEY, logo );
449         }
450         #undef AMAROK_LOGO_CACHE_KEY
451         return logo;
452     }
453 
454 } // End namespace Amarok
455