File indexing completed on 2024-05-05 04:48:48

0001 /****************************************************************************************
0002  * Copyright (c) 2008-2012 Soren Harward <stharward@gmail.com>                          *
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 #define DEBUG_PREFIX "APG::PresetModel"
0018 
0019 #include "PresetModel.h"
0020 
0021 #include "amarokconfig.h"
0022 #include "core/logger/Logger.h"
0023 #include "core/collections/Collection.h"
0024 #include "core/support/Amarok.h"
0025 #include "core/support/Components.h"
0026 #include "core/support/Debug.h"
0027 #include "core-impl/collections/support/CollectionManager.h"
0028 #include "playlistgenerator/Preset.h"
0029 #include "playlistgenerator/PresetEditDialog.h"
0030 
0031 #include <QAbstractItemModel>
0032 #include <QDesktopServices>
0033 #include <QDialog>
0034 #include <QDomDocument>
0035 #include <QDomElement>
0036 #include <QFile>
0037 #include <QList>
0038 #include <QUrl>
0039 #include <QVariant>
0040 
0041 APG::PresetModel* APG::PresetModel::s_instance = nullptr;
0042 
0043 APG::PresetModel* APG::PresetModel::instance()
0044 {
0045     if ( s_instance == nullptr ) {
0046         s_instance = new PresetModel();
0047     }
0048 
0049     return s_instance;
0050 }
0051 
0052 void
0053 APG::PresetModel::destroy()
0054 {
0055     s_instance->savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", s_instance->m_presetList );
0056     delete s_instance;
0057     s_instance = nullptr;
0058 }
0059 
0060 APG::PresetModel::PresetModel()
0061         : QAbstractListModel()
0062         , m_activePresetIndex( nullptr )
0063 {
0064     loadPresetsFromXml( Amarok::saveLocation() + "playlistgenerator.xml", true );
0065 }
0066 
0067 APG::PresetModel::~PresetModel()
0068 {
0069     while ( m_presetList.size() > 0 ) {
0070         m_presetList.takeFirst()->deleteLater();
0071     }
0072 }
0073 
0074 QVariant
0075 APG::PresetModel::data( const QModelIndex& idx, int role ) const
0076 {
0077     if ( !idx.isValid() )
0078         return QVariant();
0079 
0080     if ( idx.row() >= m_presetList.size() )
0081         return QVariant();
0082 
0083     APG::PresetPtr item = m_presetList.at( idx.row() );
0084 
0085     switch ( role ) {
0086         case Qt::DisplayRole:
0087         case Qt::EditRole:
0088             return item->title();
0089             break;
0090         default:
0091             return QVariant();
0092     }
0093 
0094     return QVariant();
0095 }
0096 
0097 QModelIndex
0098 APG::PresetModel::index( int row, int column, const QModelIndex& ) const
0099 {
0100     if ( rowCount() <= row )
0101         return QModelIndex();
0102 
0103     return createIndex( row, column);
0104 }
0105 
0106 int
0107 APG::PresetModel::rowCount( const QModelIndex& ) const
0108 {
0109     return m_presetList.size();
0110 }
0111 
0112 APG::PresetPtr
0113 APG::PresetModel::activePreset() const
0114 {
0115     if ( m_activePresetIndex && m_activePresetIndex->isValid() )
0116         return m_presetList.at( m_activePresetIndex->row() );
0117     else
0118         return APG::PresetPtr();
0119 }
0120 
0121 void
0122 APG::PresetModel::addNew()
0123 {
0124     insertPreset( APG::Preset::createNew() );
0125 }
0126 
0127 void
0128 APG::PresetModel::edit()
0129 {
0130     editPreset( createIndex( m_activePresetIndex->row(), 0 ) );
0131 }
0132 
0133 void
0134 APG::PresetModel::editPreset( const QModelIndex& index )
0135 {
0136     // TODO: possible enhancement: instead of using a modal dialog, use a QMap that allows
0137     // only one dialog per preset to be open at once
0138     PresetPtr ps = m_presetList.at( index.row() );
0139     QDialog* d = new PresetEditDialog( ps );
0140     d->exec();
0141 }
0142 
0143 void
0144 APG::PresetModel::exportActive()
0145 {
0146     auto d = new ExportDialog( activePreset() );
0147     connect( d, &ExportDialog::pleaseExport, this, &PresetModel::savePresetsToXml );
0148     d->exec();
0149 }
0150 
0151 void
0152 APG::PresetModel::import()
0153 {
0154     const QString filename = QFileDialog::getOpenFileName( nullptr, i18n("Import preset"),
0155                                                      QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
0156                                                      QStringLiteral("%1 (*.xml)").arg(i18n("Preset files") ));
0157     if( !filename.isEmpty() )
0158         loadPresetsFromXml( filename );
0159 }
0160 
0161 void
0162 APG::PresetModel::removeActive()
0163 {
0164     if ( m_presetList.size() < 1 )
0165         return;
0166 
0167     if ( m_activePresetIndex && m_activePresetIndex->isValid() ) {
0168         int row = m_activePresetIndex->row();
0169         beginRemoveRows( QModelIndex(), row, row );
0170         APG::PresetPtr p = m_presetList.takeAt( row );
0171         p->deleteLater();
0172         endRemoveRows();
0173     }
0174 }
0175 
0176 void
0177 APG::PresetModel::runGenerator( int q )
0178 {
0179     activePreset()->generate( q );
0180 }
0181 
0182 void
0183 APG::PresetModel::setActivePreset( const QModelIndex& index )
0184 {
0185     if ( m_activePresetIndex )
0186         delete m_activePresetIndex;
0187     m_activePresetIndex = new QPersistentModelIndex( index );
0188 }
0189 
0190 void
0191 APG::PresetModel::savePresetsToXmlDefault() const
0192 {
0193     savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", m_presetList );
0194 }
0195 
0196 void
0197 APG::PresetModel::savePresetsToXml( const QString& filename, const QList<APG::PresetPtr> &pl ) const
0198 {
0199     QDomDocument xmldoc;
0200     QDomElement base = xmldoc.createElement( QStringLiteral("playlistgenerator") );
0201     QList<QDomNode*> nodes;
0202     foreach ( APG::PresetPtr ps, pl ) {
0203         QDomElement* elemPtr = ps->toXml( xmldoc );
0204         base.appendChild( (*elemPtr) );
0205         nodes << elemPtr;
0206     }
0207 
0208     xmldoc.appendChild( base );
0209     QFile file( filename );
0210     if ( file.open( QIODevice::WriteOnly | QIODevice::Text ) ) {
0211         QTextStream out( &file );
0212         out.setCodec( "UTF-8" );
0213         xmldoc.save( out, 2, QDomNode::EncodingFromTextStream );
0214         if( !filename.contains( QLatin1String("playlistgenerator.xml") ) )
0215         {
0216             Amarok::Logger::longMessage( i18n("Preset exported to %1", filename),
0217                                                        Amarok::Logger::Information );
0218         }
0219     }
0220     else
0221     {
0222         Amarok::Logger::longMessage(
0223                     i18n("Preset could not be exported to %1", filename), Amarok::Logger::Error );
0224         error() << "Can not write presets to " << filename;
0225     }
0226     qDeleteAll( nodes );
0227 }
0228 
0229 void
0230 APG::PresetModel::loadPresetsFromXml( const QString& filename, bool createDefaults )
0231 {
0232     QFile file( filename );
0233     if ( file.open( QIODevice::ReadOnly ) ) {
0234         QDomDocument document;
0235         if ( document.setContent( &file ) ) {
0236             debug() << "Reading presets from" << filename;
0237             parseXmlToPresets( document );
0238         } else {
0239             error() << "Failed to read" << filename;
0240             Amarok::Logger::longMessage(
0241                         i18n("Presets could not be imported from %1", filename),
0242                         Amarok::Logger::Error );
0243         }
0244         file.close();
0245     } else {
0246         if ( !createDefaults ) {
0247             Amarok::Logger::longMessage(
0248                         i18n("%1 could not be opened for preset import", filename),
0249                         Amarok::Logger::Error );
0250         } else {
0251             QDomDocument document;
0252             QString translatedPresetExamples( presetExamples.arg(
0253                                 i18n("Example 1: new tracks added this week"),
0254                                 i18n("Example 2: rock or pop music"),
0255                                 i18n("Example 3: about one hour of tracks from different artists"),
0256                                 i18n("Example 4: like my favorite radio station"),
0257                                 i18n("Example 5: an 80-minute CD of rock, metal, and industrial") ) );
0258             document.setContent( translatedPresetExamples );
0259             debug() << "Reading built-in example presets";
0260             parseXmlToPresets( document );
0261         }
0262         error() << "Can not open" << filename;
0263     }
0264 }
0265 
0266 void
0267 APG::PresetModel::insertPreset( const APG::PresetPtr &ps )
0268 {
0269     if ( ps ) {
0270         int row = m_presetList.size();
0271         beginInsertRows( QModelIndex(), row, row );
0272         m_presetList.append( ps );
0273         endInsertRows();
0274         connect( ps.data(), &APG::Preset::lock, this, &PresetModel::lock );
0275     }
0276 }
0277 
0278 void
0279 APG::PresetModel::parseXmlToPresets( QDomDocument& document )
0280 {
0281     QDomElement rootelem = document.documentElement();
0282     for ( int i = 0; i < rootelem.childNodes().count(); i++ ) {
0283         QDomElement e = rootelem.childNodes().at( i ).toElement();
0284         if ( e.tagName() == QLatin1String("generatorpreset") ) {
0285             debug() << "creating a new generator preset";
0286             insertPreset( APG::Preset::createFromXml( e ) );
0287         } else {
0288             debug() << "Don't know what to do with tag: " << e.tagName();
0289         }
0290     }
0291 }
0292 
0293 /*
0294  * ExportDialog nested class
0295  */
0296 APG::PresetModel::ExportDialog::ExportDialog( APG::PresetPtr ps )
0297     : QFileDialog( nullptr, i18n( "Export \"%1\" preset", ps->title() ),
0298                    QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
0299                    i18n("Preset files (*.xml)") )
0300 {
0301     m_presetsToExportList.append( ps );
0302     setFileMode( QFileDialog::AnyFile );
0303     selectFile( ps->title() + ".xml" );
0304     setAcceptMode( QFileDialog::AcceptSave );
0305     connect( this, &ExportDialog::accepted, this, &ExportDialog::recvAccept );
0306 }
0307 
0308 APG::PresetModel::ExportDialog::~ExportDialog() {}
0309 
0310 void
0311 APG::PresetModel::ExportDialog::recvAccept() const
0312 {
0313     Q_EMIT pleaseExport( selectedFiles().first(), m_presetsToExportList );
0314 }
0315 
0316 const QString APG::PresetModel::presetExamples =
0317 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
0318 "<playlistgenerator>"
0319 "  <generatorpreset title=\"%1\">"
0320 "    <constrainttree>"
0321 "      <group matchtype=\"all\">"
0322 "        <constraint field=\"create date\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.8\"/>"
0323 "        <constraint field=\"play count\" comparison=\"1\" invert=\"false\" type=\"TagMatch\" value=\"\" strictness=\"1\"/>"
0324 "      </group>"
0325 "    </constrainttree>"
0326 "  </generatorpreset>"
0327 "  <generatorpreset title=\"%2\">"
0328 "    <constrainttree>"
0329 "      <group matchtype=\"any\">"
0330 "        <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
0331 "        <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Pop\" strictness=\"1\"/>"
0332 "      </group>"
0333 "    </constrainttree>"
0334 "  </generatorpreset>"
0335 "  <generatorpreset title=\"%3\">"
0336 "    <constrainttree>"
0337 "      <group matchtype=\"all\">"
0338 "        <constraint comparison=\"1\" duration=\"3600000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
0339 "        <constraint field=\"2\" type=\"PreventDuplicates\"/>"
0340 "      </group>"
0341 "    </constrainttree>"
0342 "  </generatorpreset>"
0343 "  <generatorpreset title=\"%4\">"
0344 "    <constrainttree>"
0345 "      <group matchtype=\"all\">"
0346 "        <constraint field=\"0\" type=\"PreventDuplicates\"/>"
0347 "        <constraint field=\"last played\" comparison=\"3\" invert=\"true\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.4\"/>"
0348 "        <constraint field=\"rating\" comparison=\"2\" invert=\"false\" type=\"TagMatch\" value=\"6\" strictness=\"1\"/>"
0349 "        <constraint comparison=\"1\" duration=\"10800000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
0350 "      </group>"
0351 "    </constrainttree>"
0352 "  </generatorpreset>"
0353 "  <generatorpreset title=\"%5\">"
0354 "    <constrainttree>"
0355 "      <group matchtype=\"all\">"
0356 "        <group matchtype=\"any\">"
0357 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
0358 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Metal\" strictness=\"1\"/>"
0359 "          <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Industrial\" strictness=\"1\"/>"
0360 "        </group>"
0361 "        <group matchtype=\"all\">"
0362 "          <constraint comparison=\"2\" duration=\"4500000\" type=\"PlaylistDuration\" strictness=\"0.4\"/>"
0363 "          <constraint comparison=\"0\" duration=\"4800000\" type=\"PlaylistDuration\" strictness=\"1\"/>"
0364 "        </group>"
0365 "      </group>"
0366 "    </constrainttree>"
0367 "  </generatorpreset>"
0368 "</playlistgenerator>";