1 /****************************************************************************************
2  * Copyright (c) 2007 Bart Cerneels <bart.cerneels@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 "PlaylistFileProvider.h"
18 #include "App.h"
19 #include "core-impl/playlists/types/file/PlaylistFileSupport.h"
20 #include "core/support/Amarok.h"
21 #include "core/support/Debug.h"
22 #include "core/support/Components.h"
23 #include "core/logger/Logger.h"
24 #include "core-impl/playlists/types/file/asx/ASXPlaylist.h"
25 #include "core-impl/playlists/types/file/m3u/M3UPlaylist.h"
26 #include "core-impl/playlists/types/file/pls/PLSPlaylist.h"
27 #include "core-impl/playlists/types/file/xspf/XSPFPlaylist.h"
28 #include "playlist/PlaylistModelStack.h"
29 #include "playlistmanager/PlaylistManager.h"
30 
31 #include <QAction>
32 #include <QDir>
33 #include <QInputDialog>
34 #include <QLabel>
35 #include <QString>
36 #include <QTimer>
37 #include <QUrl>
38 
39 #include <KIO/Global>
40 #include <KLocalizedString>
41 
42 using Playlist::ModelStack;
43 
44 namespace Playlists {
45 
PlaylistFileProvider()46 PlaylistFileProvider::PlaylistFileProvider()
47  : UserPlaylistProvider()
48  , m_playlistsLoaded( false )
49  , m_saveLaterTimer( 0 )
50 {
51     //playlists are lazy loaded but we can count how many we'll load already
52     QStringList keys = loadedPlaylistsConfig().keyList();
53     foreach( const QString &key, keys )
54     {
55         QUrl url( key );
56         //Don't load these from the config file, they are read from the directory anyway
57         if( KIO::upUrl(url).matches( QUrl::fromUserInput(Amarok::saveLocation(QStringLiteral("playlists"))), QUrl::StripTrailingSlash ) )
58             continue;
59         m_urlsToLoad << url;
60     }
61     //also add all files in the $KDEHOME/share/apps/amarok/playlists
62     QDir playlistDir = QDir( Amarok::saveLocation( QStringLiteral("playlists") ), QLatin1String(""),
63                              QDir::Name,
64                              QDir::Files | QDir::Readable );
65     foreach( const QString &file, playlistDir.entryList() )
66     {
67         QUrl url( playlistDir.path() );
68         url = url.adjusted(QUrl::StripTrailingSlash);
69         url.setPath(url.path() + QLatin1Char('/') + ( file ));
70         if( Playlists::isPlaylist( url ) )
71             m_urlsToLoad << url;
72     }
73 }
74 
~PlaylistFileProvider()75 PlaylistFileProvider::~PlaylistFileProvider()
76 {
77     DEBUG_BLOCK
78     //remove all, well add them again soon
79     loadedPlaylistsConfig().deleteGroup();
80     //Write loaded playlists to config file
81     foreach( Playlists::PlaylistFilePtr playlistFile, m_playlists )
82     {
83         QUrl url = playlistFile->uidUrl();
84         //only save files NOT in "playlists", those are automatically loaded.
85         if( KIO::upUrl(url).matches( QUrl::fromUserInput(Amarok::saveLocation( QStringLiteral("playlists") )), QUrl::StripTrailingSlash ) )
86             continue;
87 
88         //debug() << "storing to rc-file: " << url.url();
89 
90         loadedPlaylistsConfig().writeEntry( url.url(), playlistFile->groups() );
91     }
92     loadedPlaylistsConfig().sync();
93 }
94 
95 QString
prettyName() const96 PlaylistFileProvider::prettyName() const
97 {
98     return i18n( "Playlist Files on Disk" );
99 }
100 
icon() const101 QIcon PlaylistFileProvider::icon() const
102 {
103     return QIcon::fromTheme( "folder-documents" );
104 }
105 
106 int
playlistCount() const107 PlaylistFileProvider::playlistCount() const
108 {
109     return m_playlists.count() + m_urlsToLoad.count();
110 }
111 
112 Playlists::PlaylistList
playlists()113 PlaylistFileProvider::playlists()
114 {
115     Playlists::PlaylistList playlists;
116 
117     if( !m_playlistsLoaded )
118     {
119         //trigger a lazy load the playlists
120         QTimer::singleShot(0, this, &PlaylistFileProvider::loadPlaylists );
121         return playlists;
122     }
123 
124     foreach( const Playlists::PlaylistFilePtr &playlistFile, m_playlists )
125     {
126         Playlists::PlaylistPtr playlist = Playlists::PlaylistPtr::dynamicCast( playlistFile );
127         if( !playlist.isNull() )
128             playlists << playlist;
129     }
130     return playlists;
131 }
132 
133 Playlists::PlaylistPtr
save(const Meta::TrackList & tracks,const QString & name)134 PlaylistFileProvider::save( const Meta::TrackList &tracks, const QString &name )
135 {
136     DEBUG_BLOCK
137 
138     QString filename = name.isEmpty() ? QDateTime::currentDateTime().toString( QStringLiteral("ddd MMMM d yy hh-mm")) : name;
139     filename.replace( QLatin1Char('/'), QLatin1Char('-') );
140     filename.replace( QLatin1Char('\\'), QLatin1Char('-') );
141 
142     Playlists::PlaylistFormat format = Playlists::getFormat( QUrl::fromUserInput(filename) );
143     if( format == Playlists::Unknown ) // maybe the name just had a dot in it. We just add .xspf
144     {
145         format = Playlists::XSPF;
146         filename.append( QLatin1String( ".xspf" ) );
147     }
148 
149     QUrl path( Amarok::saveLocation( QStringLiteral("playlists") ) );
150     path = path.adjusted(QUrl::StripTrailingSlash);
151     path.setPath(path.path() + QLatin1Char('/') + ( Amarok::vfatPath( filename ) ));
152     if( QFileInfo( path.toLocalFile() ).exists() )
153     {
154         //TODO:request overwrite
155         return Playlists::PlaylistPtr();
156     }
157 
158     Playlists::PlaylistFile *playlistFile = 0;
159     switch( format )
160     {
161         case Playlists::ASX:
162             playlistFile = new Playlists::ASXPlaylist( path, this );
163             break;
164         case Playlists::PLS:
165             playlistFile = new Playlists::PLSPlaylist( path, this );
166             break;
167         case Playlists::M3U:
168             playlistFile = new Playlists::M3UPlaylist( path, this );
169             break;
170         case Playlists::XSPF:
171             playlistFile = new Playlists::XSPFPlaylist( path, this );
172             break;
173         case Playlists::XML:
174         case Playlists::RAM:
175         case Playlists::SMIL:
176         case Playlists::Unknown:
177             // this should not happen since we set the format to XSPF above.
178             return Playlists::PlaylistPtr();
179     }
180     playlistFile->setName( filename );
181     playlistFile->addTracks( tracks );
182     playlistFile->save( true );
183 
184     Playlists::PlaylistFilePtr playlistPtr( playlistFile );
185     m_playlists << playlistPtr;
186     //just in case there wasn't one loaded before.
187     m_playlistsLoaded = true;
188 
189     Playlists::PlaylistPtr playlist( playlistFile );
190     Q_EMIT playlistAdded( playlist );
191     return playlist;
192 }
193 
194 bool
import(const QUrl & path)195 PlaylistFileProvider::import( const QUrl &path )
196 {
197     DEBUG_BLOCK
198     if( !path.isValid() )
199     {
200         error() << "path is not valid!";
201         return false;
202     }
203 
204     foreach( Playlists::PlaylistFilePtr playlistFile, m_playlists )
205     {
206         if( !playlistFile )
207         {
208             error() << "Could not cast down.";
209             error() << "m_playlists got corrupted! " << __FILE__ << ":" << __LINE__;
210             continue;
211         }
212         if( playlistFile->uidUrl() == path )
213         {
214             debug() << "Playlist " << path.path() << " was already imported";
215             return false;
216         }
217     }
218 
219     debug() << "Importing playlist file " << path;
220     if( path == QUrl::fromLocalFile(Amarok::defaultPlaylistPath()) )
221     {
222         error() << "trying to load saved session playlist at %s" << path.path();
223         return false;
224     }
225 
226     Playlists::PlaylistFilePtr playlistFile = Playlists::loadPlaylistFile( path, this );
227     if( !playlistFile )
228         return false;
229 
230     m_playlists << playlistFile;
231     //just in case there wasn't one loaded before.
232     m_playlistsLoaded = true;
233 
234     Q_EMIT playlistAdded( PlaylistPtr( playlistFile.data() ) );
235     return true;
236 }
237 
238 void
renamePlaylist(PlaylistPtr playlist,const QString & newName)239 PlaylistFileProvider::renamePlaylist(PlaylistPtr playlist, const QString &newName )
240 {
241     DEBUG_BLOCK
242     playlist->setName( newName );
243 }
244 
245 bool
deletePlaylists(const Playlists::PlaylistList & playlists)246 PlaylistFileProvider::deletePlaylists( const Playlists::PlaylistList &playlists )
247 {
248     Playlists::PlaylistFileList playlistFiles;
249     foreach( Playlists::PlaylistPtr playlist, playlists )
250     {
251         Playlists::PlaylistFilePtr playlistFile =
252                 Playlists::PlaylistFilePtr::dynamicCast( playlist );
253         if( !playlistFile.isNull() )
254             playlistFiles << playlistFile;
255     }
256     return deletePlaylistFiles( playlistFiles );
257 }
258 
259 bool
deletePlaylistFiles(Playlists::PlaylistFileList playlistFiles)260 PlaylistFileProvider::deletePlaylistFiles( Playlists::PlaylistFileList playlistFiles )
261 {
262     foreach( Playlists::PlaylistFilePtr playlistFile, playlistFiles )
263     {
264         m_playlists.removeAll( playlistFile );
265         loadedPlaylistsConfig().deleteEntry( playlistFile->uidUrl().url() );
266         QFile::remove( playlistFile->uidUrl().path() );
267         Q_EMIT playlistRemoved( Playlists::PlaylistPtr::dynamicCast( playlistFile ) );
268     }
269     loadedPlaylistsConfig().sync();
270 
271     return true;
272 }
273 
274 void
loadPlaylists()275 PlaylistFileProvider::loadPlaylists()
276 {
277     if( m_urlsToLoad.isEmpty() )
278         return;
279 
280     //arbitrary number of playlists to load during one mainloop run: 5
281     for( int i = 0; i < qMin( m_urlsToLoad.count(), 5 ); i++ )
282     {
283         QUrl url = m_urlsToLoad.takeFirst();
284         QString groups = loadedPlaylistsConfig().readEntry( url.url() );
285         Playlists::PlaylistFilePtr playlist = Playlists::loadPlaylistFile( url, this );
286         if( !playlist )
287         {
288             Amarok::Logger::longMessage(
289                     i18n("The playlist file \"%1\" could not be loaded.", url.fileName() ),
290                     Amarok::Logger::Error
291                 );
292             continue;
293         }
294 
295         if( !groups.isEmpty() && playlist->isWritable() )
296             playlist->setGroups( groups.split( QLatin1Char(','),  QString::SkipEmptyParts ) );
297 
298         m_playlists << playlist;
299         Q_EMIT playlistAdded( PlaylistPtr( playlist.data() ) );
300     }
301 
302     //give the mainloop time to run
303     if( !m_urlsToLoad.isEmpty() )
304         QTimer::singleShot( 0, this, &PlaylistFileProvider::loadPlaylists );
305 }
306 
307 void
saveLater(Playlists::PlaylistFilePtr playlist)308 PlaylistFileProvider::saveLater( Playlists::PlaylistFilePtr playlist )
309 {
310     //WARNING: this assumes the playlistfile uses it's m_url for uidUrl
311     if( playlist->uidUrl().isEmpty() )
312         return;
313 
314     if( !m_saveLaterPlaylists.contains( playlist ) )
315         m_saveLaterPlaylists << playlist;
316 
317     if( !m_saveLaterTimer )
318     {
319         m_saveLaterTimer = new QTimer( this );
320         m_saveLaterTimer->setSingleShot( true );
321         m_saveLaterTimer->setInterval( 0 );
322         connect( m_saveLaterTimer, &QTimer::timeout, this, &PlaylistFileProvider::slotSaveLater );
323     }
324 
325     m_saveLaterTimer->start();
326 }
327 
328 void
slotSaveLater()329 PlaylistFileProvider::slotSaveLater() //SLOT
330 {
331     foreach( Playlists::PlaylistFilePtr playlist, m_saveLaterPlaylists )
332     {
333         playlist->save( true ); //TODO: read relative type when loading
334     }
335 
336     m_saveLaterPlaylists.clear();
337 }
338 
339 KConfigGroup
loadedPlaylistsConfig() const340 PlaylistFileProvider::loadedPlaylistsConfig() const
341 {
342     return Amarok::config( QStringLiteral("Loaded Playlist Files") );
343 }
344 
345 } //namespace Playlists
346 
347