File indexing completed on 2025-03-09 04:24:22

0001 /****************************************************************************************
0002  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
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 "IpodDeviceHelper.h"
0018 
0019 #include "core/support/Debug.h"
0020 
0021 #include <QDialogButtonBox>
0022 #include <QFile>
0023 #include <QFileInfo>
0024 
0025 #include <KConfigGroup>
0026 #include <KFormat>
0027 #include <KLocalizedString>
0028 
0029 
0030 Itdb_iTunesDB*
0031 IpodDeviceHelper::parseItdb( const QString &mountPoint, QString &errorMsg )
0032 {
0033     Itdb_iTunesDB *itdb;
0034     GError *error = nullptr;
0035 
0036     errorMsg.clear();
0037     itdb = itdb_parse( QFile::encodeName( mountPoint ), &error );
0038     if( error )
0039     {
0040         if( itdb )
0041             itdb_free( itdb );
0042         itdb = nullptr;
0043         errorMsg = QString::fromUtf8( error->message );
0044         g_error_free( error );
0045         error = nullptr;
0046     }
0047     if( !itdb && errorMsg.isEmpty() )
0048         errorMsg = i18n( "Cannot parse iTunes database due to an unreported error." );
0049     return itdb;
0050 }
0051 
0052 QString
0053 IpodDeviceHelper::collectionName( Itdb_iTunesDB *itdb )
0054 {
0055     const Itdb_IpodInfo *info = (itdb && itdb->device) ? itdb_device_get_ipod_info( itdb->device ) : nullptr;
0056     QString modelName = info ? QString::fromUtf8( itdb_info_get_ipod_model_name_string( info->ipod_model ) )
0057                              : i18nc( "iPod model that is not (yet) recognized", "Unrecognized model" );
0058 
0059     return i18nc( "Name of the iPod collection; %1 is iPod name, %2 is iPod model; example: My iPod: Nano (Blue)",
0060                   "%1: %2", IpodDeviceHelper::ipodName( itdb ), modelName );
0061 }
0062 
0063 QString
0064 IpodDeviceHelper::ipodName( Itdb_iTunesDB *itdb )
0065 {
0066     Itdb_Playlist *mpl = itdb ? itdb_playlist_mpl( itdb ) : nullptr;
0067     QString mplName = mpl ? QString::fromUtf8( mpl->name ) : QString();
0068     if( mplName.isEmpty() )
0069         mplName = i18nc( "default iPod name (when user-set name is empty)", "iPod" );
0070 
0071     return mplName;
0072 }
0073 
0074 void
0075 IpodDeviceHelper::unlinkPlaylistsTracksFromItdb( Itdb_iTunesDB *itdb )
0076 {
0077     if( !itdb )
0078         return;
0079 
0080     while( itdb->playlists )
0081     {
0082         Itdb_Playlist *ipodPlaylist = (Itdb_Playlist *) itdb->playlists->data;
0083         if( !ipodPlaylist || ipodPlaylist->itdb != itdb )
0084         {
0085             /* a) itdb_playlist_unlink() cannot work if ipodPlaylist is null, prevent
0086              *    infinite loop
0087              * b) if ipodPlaylist->itdb != itdb, something went horribly wrong. Prevent
0088              *    infinite loop even in this case
0089              */
0090             itdb->playlists = g_list_remove( itdb->playlists, ipodPlaylist );
0091             continue;
0092         }
0093         itdb_playlist_unlink( ipodPlaylist );
0094     }
0095 
0096     while( itdb->tracks )
0097     {
0098         Itdb_Track *ipodTrack = (Itdb_Track *) itdb->tracks->data;
0099         if( !ipodTrack || ipodTrack->itdb != itdb )
0100         {
0101             /* a) itdb_track_unlink() cannot work if ipodTrack is null, prevent infinite
0102              *    loop
0103              * b) if ipodTrack->itdb != itdb, something went horribly wrong. Prevent
0104              *    infinite loop even in this case
0105              */
0106             itdb->tracks = g_list_remove( itdb->tracks, ipodTrack );
0107             continue;
0108         }
0109         itdb_track_unlink( ipodTrack );
0110     }
0111 }
0112 
0113 /**
0114  * Return ipod info if iPod model is recognized, returns null if itdb is null or if iPod
0115  * is invalid or unknown.
0116  */
0117 static const Itdb_IpodInfo *getIpodInfo( const Itdb_iTunesDB *itdb )
0118 {
0119     if( !itdb || !itdb->device )
0120         return nullptr;
0121     const Itdb_IpodInfo *info = itdb_device_get_ipod_info( itdb->device );
0122     if( !info )
0123         return nullptr;
0124     if( info->ipod_model == ITDB_IPOD_MODEL_INVALID
0125      || info->ipod_model == ITDB_IPOD_MODEL_UNKNOWN )
0126     {
0127         return nullptr;
0128     }
0129     return info;
0130 }
0131 
0132 static bool
0133 firewireGuidNeeded( const Itdb_IpodGeneration &generation )
0134 {
0135     switch( generation )
0136     {
0137         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
0138         // not nice, but should not change, no new devices use hash58
0139         case ITDB_IPOD_GENERATION_CLASSIC_1:
0140         case ITDB_IPOD_GENERATION_CLASSIC_2:
0141         case ITDB_IPOD_GENERATION_CLASSIC_3:
0142         case ITDB_IPOD_GENERATION_NANO_3:
0143         case ITDB_IPOD_GENERATION_NANO_4:
0144             return true; // ITDB_CHECKSUM_HASH58
0145         default:
0146             break;
0147     }
0148     return false;
0149 }
0150 
0151 static bool
0152 hashInfoNeeded( const Itdb_IpodGeneration &generation )
0153 {
0154     switch( generation )
0155     {
0156         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
0157         // not nice, but should not change, current devices need libhashab
0158         case ITDB_IPOD_GENERATION_NANO_5:
0159         case ITDB_IPOD_GENERATION_TOUCH_1:
0160         case ITDB_IPOD_GENERATION_TOUCH_2:
0161         case ITDB_IPOD_GENERATION_TOUCH_3:
0162         case ITDB_IPOD_GENERATION_IPHONE_1:
0163         case ITDB_IPOD_GENERATION_IPHONE_2:
0164         case ITDB_IPOD_GENERATION_IPHONE_3:
0165             return true; // ITDB_CHECKSUM_HASH72
0166         default:
0167             break;
0168     }
0169     return false;
0170 }
0171 
0172 static bool
0173 hashAbNeeded( const Itdb_IpodGeneration &generation )
0174 {
0175     switch( generation )
0176     {
0177         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
0178         // TODO: not nice, new released devices may be added!
0179         case ITDB_IPOD_GENERATION_IPAD_1:
0180         case ITDB_IPOD_GENERATION_IPHONE_4:
0181         case ITDB_IPOD_GENERATION_TOUCH_4:
0182         case ITDB_IPOD_GENERATION_NANO_6:
0183             return true; // ITDB_CHECKSUM_HASHAB
0184         default:
0185             break;
0186     }
0187     return false;
0188 }
0189 
0190 /**
0191  * Returns true if file @param relFilename is found, readable and nonempty.
0192  * Searches in @param mountPoint /iPod_Control/Device/
0193  */
0194 static bool
0195 fileFound( const QString &mountPoint, const QString &relFilename )
0196 {
0197     gchar *controlDir = itdb_get_device_dir( QFile::encodeName( mountPoint ) );
0198     if( !controlDir )
0199         return false;
0200     QString absFilename = QStringLiteral( "%1/%2" ).arg( QFile::decodeName( controlDir ),
0201                                                   relFilename );
0202     g_free( controlDir );
0203 
0204     QFileInfo fileInfo( absFilename );
0205     return fileInfo.isReadable() && fileInfo.size() > 0;
0206 }
0207 
0208 static bool
0209 safeToWriteWithMessage( const QString &mountPoint, const Itdb_iTunesDB *itdb, QString &message )
0210 {
0211     const Itdb_IpodInfo *info = getIpodInfo( itdb ); // returns null on null itdb
0212     if( !info )
0213     {
0214         message = i18n( "iPod model was not recognized." );
0215         return false;
0216     }
0217 
0218     QString gen = QString::fromUtf8( itdb_info_get_ipod_generation_string( info->ipod_generation ) );
0219     if( firewireGuidNeeded( info->ipod_generation ) )
0220     {
0221         // okay FireWireGUID may be in plain SysInfo, too, but it's hard to check and
0222         // error-prone so we just require SysInfoExtended which is machine-generated
0223         const QString sysInfoExtended( "SysInfoExtended" );
0224         bool sysInfoExtendedExists = fileFound( mountPoint, sysInfoExtended );
0225         message += ( sysInfoExtendedExists )
0226                    ? i18n( "%1 family uses %2 file to generate correct database checksum.",
0227                            gen, sysInfoExtended )
0228                    : i18n( "%1 family needs %2 file to generate correct database checksum.",
0229                            gen, sysInfoExtended );
0230         if( !sysInfoExtendedExists )
0231             return false;
0232     }
0233     if( hashInfoNeeded( info->ipod_generation ) )
0234     {
0235         const QString hashInfo( "HashInfo" );
0236         bool hashInfoExists = fileFound( mountPoint, hashInfo );
0237         message += hashInfoExists
0238                    ? i18n( "%1 family uses %2 file to generate correct database checksum.",
0239                            gen, hashInfo )
0240                    : i18n( "%1 family needs %2 file to generate correct database checksum.",
0241                            gen, hashInfo );
0242         if( !hashInfoExists )
0243             return false;
0244     }
0245     if( hashAbNeeded( info->ipod_generation ) )
0246     {
0247         message += i18nc( "Do not translate hash-AB, libgpod, libhashab.so",
0248             "%1 family probably uses hash-AB to generate correct database checksum. "
0249             "libgpod (as of version 0.8.2) doesn't know how to compute it, but tries "
0250             "to dynamically load external library libhashab.so to do it.", gen
0251         );
0252         // we don't return false, user may have hash-AB support installed
0253     }
0254     return true;
0255 }
0256 
0257 static void
0258 fillInModelComboBox( QComboBox *comboBox, bool someSysInfoFound )
0259 {
0260     if( someSysInfoFound )
0261     {
0262         comboBox->addItem( i18n( "Autodetect (%1 file(s) present)", QString( "SysInfo") ), QString() );
0263         comboBox->setEnabled( false );
0264         return;
0265     }
0266 
0267     const Itdb_IpodInfo *info = itdb_info_get_ipod_info_table();
0268     if( !info )
0269     {
0270         // this is not i18n-ed for purpose: it should never happen
0271         comboBox->addItem( QStringLiteral( "Failed to get iPod info table!" ), QString() );
0272         return;
0273     }
0274 
0275     while( info->model_number )
0276     {
0277         QString generation = QString::fromUtf8( itdb_info_get_ipod_generation_string( info->ipod_generation) );
0278         QString capacity = KFormat().formatByteSize( info->capacity * 1073741824.0, 0 );
0279         QString modelName = QString::fromUtf8( itdb_info_get_ipod_model_name_string( info->ipod_model ) );
0280         QString modelNumber = QString::fromUtf8( info->model_number );
0281         QString label = i18nc( "Examples: "
0282                                "%1: Nano with camera (5th Gen.); [generation]"
0283                                "%2: 16 GiB; [capacity]"
0284                                "%3: Nano (Orange); [model name]"
0285                                "%4: A123 [model number]",
0286                                "%1: %2 %3 [%4]",
0287                                generation, capacity, modelName, modelNumber );
0288         comboBox->addItem( label, modelNumber );
0289         info++; // list is ended by null-filled info
0290     }
0291     comboBox->setMaxVisibleItems( 16 );
0292 }
0293 
0294 void
0295 IpodDeviceHelper::fillInConfigureDialog( QDialog *configureDialog,
0296                                          Ui::IpodConfiguration *configureDialogUi,
0297                                          const QString &mountPoint,
0298                                          Itdb_iTunesDB *itdb,
0299                                          const Transcoding::Configuration &transcodeConfig,
0300                                          const QString &errorMessage )
0301 {
0302     static const QString unknown = i18nc( "Unknown iPod model, generation...", "Unknown" );
0303     static const QString supported = i18nc( "In a dialog: Video: Supported", "Supported" );
0304     static const QString notSupported = i18nc( "In a dialog: Video: Not supported", "Not supported" );
0305     static const QString present = i18nc( "In a dialog: Some file: Present", "Present" );
0306     static const QString notFound = i18nc( "In a dialog: Some file: Not found", "<b>Not found</b>" );
0307     static const QString notNeeded = i18nc( "In a dialog: Some file: Not needed", "Not needed" );
0308 
0309     // following call accepts null itdb
0310     configureDialogUi->nameLineEdit->setText( IpodDeviceHelper::ipodName( itdb ) );
0311     QString notes;
0312     QString warningText;
0313     QString safeToWriteMessage;
0314     bool isSafeToWrite = safeToWriteWithMessage( mountPoint, itdb, safeToWriteMessage );
0315     bool sysInfoExtendedExists = fileFound( mountPoint, "SysInfoExtended" );
0316     bool sysInfoExists = fileFound( mountPoint, "SysInfo" );
0317 
0318     if( itdb )
0319     {
0320         configureDialogUi->nameLineEdit->setEnabled( isSafeToWrite );
0321         configureDialogUi->transcodeComboBox->setEnabled( isSafeToWrite );
0322         configureDialogUi->transcodeComboBox->fillInChoices( transcodeConfig );
0323         configureDialogUi->modelComboLabel->setEnabled( false );
0324         configureDialogUi->modelComboBox->setEnabled( false );
0325         configureDialogUi->initializeLabel->setEnabled( false );
0326         configureDialogUi->initializeButton->setEnabled( false );
0327         if( !errorMessage.isEmpty() )
0328             // to inform user about successful initialization.
0329             warningText = QString( "<b>%1</b>" ).arg( errorMessage );
0330 
0331         const Itdb_Device *device = itdb->device;
0332         const Itdb_IpodInfo *info = device ? itdb_device_get_ipod_info( device ) : nullptr;
0333         configureDialogUi->infoGroupBox->setEnabled( true );
0334         configureDialogUi->modelPlaceholer->setText( info ? QString::fromUtf8(
0335             itdb_info_get_ipod_model_name_string( info->ipod_model ) ) : unknown );
0336         configureDialogUi->generationPlaceholder->setText( info ? QString::fromUtf8(
0337             itdb_info_get_ipod_generation_string( info->ipod_generation ) ) : unknown );
0338         configureDialogUi->videoPlaceholder->setText( device ?
0339             ( itdb_device_supports_video( device ) ? supported : notSupported ) : unknown );
0340         configureDialogUi->albumArtworkPlaceholder->setText( device ?
0341             ( itdb_device_supports_artwork( device ) ? supported : notSupported ) : unknown );
0342 
0343         if( isSafeToWrite )
0344             notes += safeToWriteMessage; // may be empty, doesn't hurt
0345         else
0346         {
0347             Q_ASSERT( !safeToWriteMessage.isEmpty() );
0348             const QString link( "http://gtkpod.git.sourceforge.net/git/gitweb.cgi?p=gtkpod/libgpod;a=blob_plain;f=README.overview" );
0349             notes += i18nc( "%1 is informational sentence giving reason",
0350                 "<b>%1</b><br><br>"
0351                 "As a safety measure, Amarok will <i>refuse to perform any writes</i> to "
0352                 "iPod. (modifying iTunes database could make it look empty from the device "
0353                 "point of view)<br>"
0354                 "See <a href='%2'>README.overview</a> file from libgpod source repository "
0355                 "for more information.",
0356                 safeToWriteMessage, link
0357             );
0358         }
0359     }
0360     else
0361     {
0362         configureDialogUi->nameLineEdit->setEnabled( true ); // for initialization
0363         configureDialogUi->modelComboLabel->setEnabled( true );
0364         configureDialogUi->modelComboBox->setEnabled( true );
0365         if( configureDialogUi->modelComboBox->count() == 0 )
0366             fillInModelComboBox( configureDialogUi->modelComboBox, sysInfoExists || sysInfoExtendedExists );
0367         configureDialogUi->initializeLabel->setEnabled( true );
0368         configureDialogUi->initializeButton->setEnabled( true );
0369         configureDialogUi->initializeButton->setIcon( QIcon::fromTheme( "task-attention" ) );
0370         if( !errorMessage.isEmpty() )
0371             warningText = i18n(
0372                 "<b>%1</b><br><br>"
0373                 "Above problem prevents Amarok from using your iPod. You can try to "
0374                 "re-create critical iPod folders and files (including iTunes database) "
0375                 "using the <b>%2</b> button below.<br><br> "
0376                 "Initializing iPod <b>destroys iPod track and photo database</b>, however "
0377                 "it should not delete any tracks. The tracks will become orphaned.",
0378                 errorMessage,
0379                 configureDialogUi->initializeButton->text().remove( QChar('&') )
0380             );
0381 
0382         configureDialogUi->infoGroupBox->setEnabled( false );
0383         configureDialogUi->modelPlaceholer->setText(  unknown );
0384         configureDialogUi->generationPlaceholder->setText(  unknown );
0385         configureDialogUi->videoPlaceholder->setText(  unknown );
0386         configureDialogUi->albumArtworkPlaceholder->setText( unknown );
0387     }
0388 
0389     if( !warningText.isEmpty() )
0390     {
0391         configureDialogUi->initializeLabel->setText( warningText );
0392         configureDialogUi->initializeLabel->adjustSize();
0393     }
0394 
0395     QString sysInfoExtendedString = sysInfoExtendedExists ? present : notFound;
0396     QString sysInfoString = sysInfoExists ? present :
0397                           ( sysInfoExtendedExists ? notNeeded : notFound );
0398 
0399     configureDialogUi->sysInfoPlaceholder->setText( sysInfoString );
0400     configureDialogUi->sysInfoExtendedPlaceholder->setText( sysInfoExtendedString );
0401     configureDialogUi->notesPlaceholder->setText( notes );
0402     configureDialogUi->notesPlaceholder->adjustSize();
0403 
0404     configureDialog->findChild<QDialogButtonBox*>()->button( QDialogButtonBox::Ok )->setEnabled( isSafeToWrite );
0405 }
0406 
0407 bool
0408 IpodDeviceHelper::initializeIpod( const QString &mountPoint,
0409                                   const Ui::IpodConfiguration *configureDialogUi,
0410                                   QString &errorMessage )
0411 {
0412     DEBUG_BLOCK
0413     bool success = true;
0414 
0415     int currentModelIndex = configureDialogUi->modelComboBox->currentIndex();
0416     QByteArray modelNumber = configureDialogUi->modelComboBox->itemData( currentModelIndex ).toString().toUtf8();
0417     if( !modelNumber.isEmpty() )
0418     {
0419         modelNumber.prepend( 'x' );  // ModelNumStr should start with x
0420         const char *modelNumberRaw = modelNumber.constData();
0421         Itdb_Device *device = itdb_device_new();
0422         // following call reads existing SysInfo
0423         itdb_device_set_mountpoint( device, QFile::encodeName( mountPoint ) );
0424         const char *field = "ModelNumStr";
0425         debug() << "Setting SysInfo field" << field << "to value" << modelNumberRaw;
0426         itdb_device_set_sysinfo( device, field, modelNumberRaw );
0427         GError *error = nullptr;
0428         success = itdb_device_write_sysinfo( device, &error );
0429         if( !success )
0430         {
0431             if( error )
0432             {
0433                 errorMessage = i18nc( "Do not translate SysInfo",
0434                                       "Failed to write SysInfo: %1", error->message );
0435                 g_error_free( error );
0436             }
0437             else
0438                 errorMessage = i18nc( "Do not translate SysInfo",
0439                     "Failed to write SysInfo file due to an unreported error" );
0440         }
0441         itdb_device_free( device );
0442         if( !success )
0443             return success;
0444     }
0445 
0446     QString name = configureDialogUi->nameLineEdit->text();
0447     if( name.isEmpty() )
0448         name = ipodName( nullptr ); // return fallback name
0449 
0450     GError *error = nullptr;
0451     success = itdb_init_ipod( QFile::encodeName( mountPoint ), nullptr /* model number */,
0452                               name.toUtf8(), &error );
0453     errorMessage.clear();
0454     if( error )
0455     {
0456         errorMessage = QString::fromUtf8( error->message );
0457         g_error_free( error );
0458         error = nullptr;
0459     }
0460     if( !success && errorMessage.isEmpty() )
0461         errorMessage = i18n( "Cannot initialize iPod due to an unreported error." );
0462     return success;
0463 }
0464 
0465 void
0466 IpodDeviceHelper::setIpodName( Itdb_iTunesDB *itdb, const QString &newName )
0467 {
0468     if( !itdb )
0469         return;
0470     Itdb_Playlist *mpl = itdb_playlist_mpl( itdb );
0471     if( !mpl )
0472         return;
0473     g_free( mpl->name );
0474     mpl->name = g_strdup( newName.toUtf8() );
0475 }
0476 
0477 bool
0478 IpodDeviceHelper::safeToWrite( const QString &mountPoint, const Itdb_iTunesDB *itdb )
0479 {
0480     QString dummyMessage;
0481     return safeToWriteWithMessage( mountPoint, itdb, dummyMessage );
0482 }