1 /****************************************************************************************
2  * Copyright (c) 2008-2012 Soren Harward <stharward@gmail.com>                          *
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 #define DEBUG_PREFIX "APG::PresetModel"
18 
19 #include "PresetModel.h"
20 
21 #include "amarokconfig.h"
22 #include "core/logger/Logger.h"
23 #include "core/collections/Collection.h"
24 #include "core/support/Amarok.h"
25 #include "core/support/Components.h"
26 #include "core/support/Debug.h"
27 #include "core-impl/collections/support/CollectionManager.h"
28 #include "playlistgenerator/Preset.h"
29 #include "playlistgenerator/PresetEditDialog.h"
30 
31 #include <QAbstractItemModel>
32 #include <QDesktopServices>
33 #include <QDialog>
34 #include <QDomDocument>
35 #include <QDomElement>
36 #include <QFile>
37 #include <QList>
38 #include <QUrl>
39 #include <QVariant>
40 
41 APG::PresetModel* APG::PresetModel::s_instance = nullptr;
42 
instance()43 APG::PresetModel* APG::PresetModel::instance()
44 {
45     if ( s_instance == nullptr ) {
46         s_instance = new PresetModel();
47     }
48 
49     return s_instance;
50 }
51 
52 void
destroy()53 APG::PresetModel::destroy()
54 {
55     s_instance->savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", s_instance->m_presetList );
56     delete s_instance;
57     s_instance = nullptr;
58 }
59 
PresetModel()60 APG::PresetModel::PresetModel()
61         : QAbstractListModel()
62         , m_activePresetIndex( 0 )
63 {
64     loadPresetsFromXml( Amarok::saveLocation() + "playlistgenerator.xml", true );
65 }
66 
~PresetModel()67 APG::PresetModel::~PresetModel()
68 {
69     while ( m_presetList.size() > 0 ) {
70         m_presetList.takeFirst()->deleteLater();
71     }
72 }
73 
74 QVariant
data(const QModelIndex & idx,int role) const75 APG::PresetModel::data( const QModelIndex& idx, int role ) const
76 {
77     if ( !idx.isValid() )
78         return QVariant();
79 
80     if ( idx.row() >= m_presetList.size() )
81         return QVariant();
82 
83     APG::PresetPtr item = m_presetList.at( idx.row() );
84 
85     switch ( role ) {
86         case Qt::DisplayRole:
87         case Qt::EditRole:
88             return item->title();
89             break;
90         default:
91             return QVariant();
92     }
93 
94     return QVariant();
95 }
96 
97 QModelIndex
index(int row,int column,const QModelIndex &) const98 APG::PresetModel::index( int row, int column, const QModelIndex& ) const
99 {
100     if ( rowCount() <= row )
101         return QModelIndex();
102 
103     return createIndex( row, column);
104 }
105 
106 int
rowCount(const QModelIndex &) const107 APG::PresetModel::rowCount( const QModelIndex& ) const
108 {
109     return m_presetList.size();
110 }
111 
112 APG::PresetPtr
activePreset() const113 APG::PresetModel::activePreset() const
114 {
115     if ( m_activePresetIndex && m_activePresetIndex->isValid() )
116         return m_presetList.at( m_activePresetIndex->row() );
117     else
118         return APG::PresetPtr();
119 }
120 
121 void
addNew()122 APG::PresetModel::addNew()
123 {
124     insertPreset( APG::Preset::createNew() );
125 }
126 
127 void
edit()128 APG::PresetModel::edit()
129 {
130     editPreset( createIndex( m_activePresetIndex->row(), 0 ) );
131 }
132 
133 void
editPreset(const QModelIndex & index)134 APG::PresetModel::editPreset( const QModelIndex& index )
135 {
136     // TODO: possible enhancement: instead of using a modal dialog, use a QMap that allows
137     // only one dialog per preset to be open at once
138     PresetPtr ps = m_presetList.at( index.row() );
139     QDialog* d = new PresetEditDialog( ps );
140     d->exec();
141 }
142 
143 void
exportActive()144 APG::PresetModel::exportActive()
145 {
146     auto d = new ExportDialog( activePreset() );
147     connect( d, &ExportDialog::pleaseExport, this, &PresetModel::savePresetsToXml );
148     d->exec();
149 }
150 
151 void
import()152 APG::PresetModel::import()
153 {
154     const QString filename = QFileDialog::getOpenFileName( nullptr, i18n("Import preset"),
155                                                      QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
156                                                      QStringLiteral("%1 (*.xml)").arg(i18n("Preset files") ));
157     if( !filename.isEmpty() )
158         loadPresetsFromXml( filename );
159 }
160 
161 void
removeActive()162 APG::PresetModel::removeActive()
163 {
164     if ( m_presetList.size() < 1 )
165         return;
166 
167     if ( m_activePresetIndex && m_activePresetIndex->isValid() ) {
168         int row = m_activePresetIndex->row();
169         beginRemoveRows( QModelIndex(), row, row );
170         APG::PresetPtr p = m_presetList.takeAt( row );
171         p->deleteLater();
172         endRemoveRows();
173     }
174 }
175 
176 void
runGenerator(int q)177 APG::PresetModel::runGenerator( int q )
178 {
179     activePreset()->generate( q );
180 }
181 
182 void
setActivePreset(const QModelIndex & index)183 APG::PresetModel::setActivePreset( const QModelIndex& index )
184 {
185     if ( m_activePresetIndex )
186         delete m_activePresetIndex;
187     m_activePresetIndex = new QPersistentModelIndex( index );
188 }
189 
190 void
savePresetsToXmlDefault() const191 APG::PresetModel::savePresetsToXmlDefault() const
192 {
193     savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", m_presetList );
194 }
195 
196 void
savePresetsToXml(const QString & filename,const QList<APG::PresetPtr> & pl) const197 APG::PresetModel::savePresetsToXml( const QString& filename, const QList<APG::PresetPtr> &pl ) const
198 {
199     QDomDocument xmldoc;
200     QDomElement base = xmldoc.createElement( QStringLiteral("playlistgenerator") );
201     QList<QDomNode*> nodes;
202     foreach ( APG::PresetPtr ps, pl ) {
203         QDomElement* elemPtr = ps->toXml( xmldoc );
204         base.appendChild( (*elemPtr) );
205         nodes << elemPtr;
206     }
207 
208     xmldoc.appendChild( base );
209     QFile file( filename );
210     if ( file.open( QIODevice::WriteOnly | QIODevice::Text ) ) {
211         QTextStream out( &file );
212         out.setCodec( "UTF-8" );
213         xmldoc.save( out, 2, QDomNode::EncodingFromTextStream );
214         if( !filename.contains( QLatin1String("playlistgenerator.xml") ) )
215         {
216             Amarok::Logger::longMessage( i18n("Preset exported to %1", filename),
217                                                        Amarok::Logger::Information );
218         }
219     }
220     else
221     {
222         Amarok::Logger::longMessage(
223                     i18n("Preset could not be exported to %1", filename), Amarok::Logger::Error );
224         error() << "Can not write presets to " << filename;
225     }
226     qDeleteAll( nodes );
227 }
228 
229 void
loadPresetsFromXml(const QString & filename,bool createDefaults)230 APG::PresetModel::loadPresetsFromXml( const QString& filename, bool createDefaults )
231 {
232     QFile file( filename );
233     if ( file.open( QIODevice::ReadOnly ) ) {
234         QDomDocument document;
235         if ( document.setContent( &file ) ) {
236             debug() << "Reading presets from" << filename;
237             parseXmlToPresets( document );
238         } else {
239             error() << "Failed to read" << filename;
240             Amarok::Logger::longMessage(
241                         i18n("Presets could not be imported from %1", filename),
242                         Amarok::Logger::Error );
243         }
244         file.close();
245     } else {
246         if ( !createDefaults ) {
247             Amarok::Logger::longMessage(
248                         i18n("%1 could not be opened for preset import", filename),
249                         Amarok::Logger::Error );
250         } else {
251             QDomDocument document;
252             QString translatedPresetExamples( presetExamples.arg(
253                                 i18n("Example 1: new tracks added this week"),
254                                 i18n("Example 2: rock or pop music"),
255                                 i18n("Example 3: about one hour of tracks from different artists"),
256                                 i18n("Example 4: like my favorite radio station"),
257                                 i18n("Example 5: an 80-minute CD of rock, metal, and industrial") ) );
258             document.setContent( translatedPresetExamples );
259             debug() << "Reading built-in example presets";
260             parseXmlToPresets( document );
261         }
262         error() << "Can not open" << filename;
263     }
264 }
265 
266 void
insertPreset(const APG::PresetPtr & ps)267 APG::PresetModel::insertPreset( const APG::PresetPtr &ps )
268 {
269     if ( ps ) {
270         int row = m_presetList.size();
271         beginInsertRows( QModelIndex(), row, row );
272         m_presetList.append( ps );
273         endInsertRows();
274         connect( ps.data(), &APG::Preset::lock, this, &PresetModel::lock );
275     }
276 }
277 
278 void
parseXmlToPresets(QDomDocument & document)279 APG::PresetModel::parseXmlToPresets( QDomDocument& document )
280 {
281     QDomElement rootelem = document.documentElement();
282     for ( int i = 0; i < rootelem.childNodes().count(); i++ ) {
283         QDomElement e = rootelem.childNodes().at( i ).toElement();
284         if ( e.tagName() == QLatin1String("generatorpreset") ) {
285             debug() << "creating a new generator preset";
286             insertPreset( APG::Preset::createFromXml( e ) );
287         } else {
288             debug() << "Don't know what to do with tag: " << e.tagName();
289         }
290     }
291 }
292 
293 /*
294  * ExportDialog nested class
295  */
ExportDialog(APG::PresetPtr ps)296 APG::PresetModel::ExportDialog::ExportDialog( APG::PresetPtr ps )
297     : QFileDialog( nullptr, i18n( "Export \"%1\" preset", ps->title() ),
298                    QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
299                    i18n("Preset files (*.xml)") )
300 {
301     m_presetsToExportList.append( ps );
302     setFileMode( QFileDialog::AnyFile );
303     selectFile( ps->title() + ".xml" );
304     setAcceptMode( QFileDialog::AcceptSave );
305     connect( this, &ExportDialog::accepted, this, &ExportDialog::recvAccept );
306 }
307 
~ExportDialog()308 APG::PresetModel::ExportDialog::~ExportDialog() {}
309 
310 void
recvAccept() const311 APG::PresetModel::ExportDialog::recvAccept() const
312 {
313     Q_EMIT pleaseExport( selectedFiles().first(), m_presetsToExportList );
314 }
315 
316 const QString APG::PresetModel::presetExamples =
317 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
318 "<playlistgenerator>"
319 "  <generatorpreset title=\"%1\">"
320 "    <constrainttree>"
321 "      <group matchtype=\"all\">"
322 "        <constraint field=\"create date\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.8\"/>"
323 "        <constraint field=\"play count\" comparison=\"1\" invert=\"false\" type=\"TagMatch\" value=\"\" strictness=\"1\"/>"
324 "      </group>"
325 "    </constrainttree>"
326 "  </generatorpreset>"
327 "  <generatorpreset title=\"%2\">"
328 "    <constrainttree>"
329 "      <group matchtype=\"any\">"
330 "        <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
331 "        <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Pop\" strictness=\"1\"/>"
332 "      </group>"
333 "    </constrainttree>"
334 "  </generatorpreset>"
335 "  <generatorpreset title=\"%3\">"
336 "    <constrainttree>"
337 "      <group matchtype=\"all\">"
338 "        <constraint comparison=\"1\" duration=\"3600000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
339 "        <constraint field=\"2\" type=\"PreventDuplicates\"/>"
340 "      </group>"
341 "    </constrainttree>"
342 "  </generatorpreset>"
343 "  <generatorpreset title=\"%4\">"
344 "    <constrainttree>"
345 "      <group matchtype=\"all\">"
346 "        <constraint field=\"0\" type=\"PreventDuplicates\"/>"
347 "        <constraint field=\"last played\" comparison=\"3\" invert=\"true\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.4\"/>"
348 "        <constraint field=\"rating\" comparison=\"2\" invert=\"false\" type=\"TagMatch\" value=\"6\" strictness=\"1\"/>"
349 "        <constraint comparison=\"1\" duration=\"10800000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
350 "      </group>"
351 "    </constrainttree>"
352 "  </generatorpreset>"
353 "  <generatorpreset title=\"%5\">"
354 "    <constrainttree>"
355 "      <group matchtype=\"all\">"
356 "        <group matchtype=\"any\">"
357 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
358 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Metal\" strictness=\"1\"/>"
359 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Industrial\" strictness=\"1\"/>"
360 "        </group>"
361 "        <group matchtype=\"all\">"
362 "          <constraint comparison=\"2\" duration=\"4500000\" type=\"PlaylistDuration\" strictness=\"0.4\"/>"
363 "          <constraint comparison=\"0\" duration=\"4800000\" type=\"PlaylistDuration\" strictness=\"1\"/>"
364 "        </group>"
365 "      </group>"
366 "    </constrainttree>"
367 "  </generatorpreset>"
368 "</playlistgenerator>";
369