1 /****************************************************************************************
2  * Copyright (c) 2012 Matěj Laitl <matej@laitl.cz>                                      *
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 "IpodDeviceHelper.h"
18 
19 #include "core/support/Debug.h"
20 
21 #include <QDialogButtonBox>
22 #include <QFile>
23 #include <QFileInfo>
24 
25 #include <KConfigGroup>
26 #include <KFormat>
27 #include <KLocalizedString>
28 
29 
30 Itdb_iTunesDB*
parseItdb(const QString & mountPoint,QString & errorMsg)31 IpodDeviceHelper::parseItdb( const QString &mountPoint, QString &errorMsg )
32 {
33     Itdb_iTunesDB *itdb;
34     GError *error = 0;
35 
36     errorMsg.clear();
37     itdb = itdb_parse( QFile::encodeName( mountPoint ), &error );
38     if( error )
39     {
40         if( itdb )
41             itdb_free( itdb );
42         itdb = 0;
43         errorMsg = QString::fromUtf8( error->message );
44         g_error_free( error );
45         error = 0;
46     }
47     if( !itdb && errorMsg.isEmpty() )
48         errorMsg = i18n( "Cannot parse iTunes database due to an unreported error." );
49     return itdb;
50 }
51 
52 QString
collectionName(Itdb_iTunesDB * itdb)53 IpodDeviceHelper::collectionName( Itdb_iTunesDB *itdb )
54 {
55     const Itdb_IpodInfo *info = (itdb && itdb->device) ? itdb_device_get_ipod_info( itdb->device ) : 0;
56     QString modelName = info ? QString::fromUtf8( itdb_info_get_ipod_model_name_string( info->ipod_model ) )
57                              : i18nc( "iPod model that is not (yet) recognized", "Unrecognized model" );
58 
59     return i18nc( "Name of the iPod collection; %1 is iPod name, %2 is iPod model; example: My iPod: Nano (Blue)",
60                   "%1: %2", IpodDeviceHelper::ipodName( itdb ), modelName );
61 }
62 
63 QString
ipodName(Itdb_iTunesDB * itdb)64 IpodDeviceHelper::ipodName( Itdb_iTunesDB *itdb )
65 {
66     Itdb_Playlist *mpl = itdb ? itdb_playlist_mpl( itdb ) : 0;
67     QString mplName = mpl ? QString::fromUtf8( mpl->name ) : QString();
68     if( mplName.isEmpty() )
69         mplName = i18nc( "default iPod name (when user-set name is empty)", "iPod" );
70 
71     return mplName;
72 }
73 
74 void
unlinkPlaylistsTracksFromItdb(Itdb_iTunesDB * itdb)75 IpodDeviceHelper::unlinkPlaylistsTracksFromItdb( Itdb_iTunesDB *itdb )
76 {
77     if( !itdb )
78         return;
79 
80     while( itdb->playlists )
81     {
82         Itdb_Playlist *ipodPlaylist = (Itdb_Playlist *) itdb->playlists->data;
83         if( !ipodPlaylist || ipodPlaylist->itdb != itdb )
84         {
85             /* a) itdb_playlist_unlink() cannot work if ipodPlaylist is null, prevent
86              *    infinite loop
87              * b) if ipodPlaylist->itdb != itdb, something went horribly wrong. Prevent
88              *    infinite loop even in this case
89              */
90             itdb->playlists = g_list_remove( itdb->playlists, ipodPlaylist );
91             continue;
92         }
93         itdb_playlist_unlink( ipodPlaylist );
94     }
95 
96     while( itdb->tracks )
97     {
98         Itdb_Track *ipodTrack = (Itdb_Track *) itdb->tracks->data;
99         if( !ipodTrack || ipodTrack->itdb != itdb )
100         {
101             /* a) itdb_track_unlink() cannot work if ipodTrack is null, prevent infinite
102              *    loop
103              * b) if ipodTrack->itdb != itdb, something went horribly wrong. Prevent
104              *    infinite loop even in this case
105              */
106             itdb->tracks = g_list_remove( itdb->tracks, ipodTrack );
107             continue;
108         }
109         itdb_track_unlink( ipodTrack );
110     }
111 }
112 
113 /**
114  * Return ipod info if iPod model is recognized, returns null if itdb is null or if iPod
115  * is invalid or unknown.
116  */
getIpodInfo(const Itdb_iTunesDB * itdb)117 static const Itdb_IpodInfo *getIpodInfo( const Itdb_iTunesDB *itdb )
118 {
119     if( !itdb || !itdb->device )
120         return 0;
121     const Itdb_IpodInfo *info = itdb_device_get_ipod_info( itdb->device );
122     if( !info )
123         return 0;
124     if( info->ipod_model == ITDB_IPOD_MODEL_INVALID
125      || info->ipod_model == ITDB_IPOD_MODEL_UNKNOWN )
126     {
127         return 0;
128     }
129     return info;
130 }
131 
132 static bool
firewireGuidNeeded(const Itdb_IpodGeneration & generation)133 firewireGuidNeeded( const Itdb_IpodGeneration &generation )
134 {
135     switch( generation )
136     {
137         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
138         // not nice, but should not change, no new devices use hash58
139         case ITDB_IPOD_GENERATION_CLASSIC_1:
140         case ITDB_IPOD_GENERATION_CLASSIC_2:
141         case ITDB_IPOD_GENERATION_CLASSIC_3:
142         case ITDB_IPOD_GENERATION_NANO_3:
143         case ITDB_IPOD_GENERATION_NANO_4:
144             return true; // ITDB_CHECKSUM_HASH58
145         default:
146             break;
147     }
148     return false;
149 }
150 
151 static bool
hashInfoNeeded(const Itdb_IpodGeneration & generation)152 hashInfoNeeded( const Itdb_IpodGeneration &generation )
153 {
154     switch( generation )
155     {
156         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
157         // not nice, but should not change, current devices need libhashab
158         case ITDB_IPOD_GENERATION_NANO_5:
159         case ITDB_IPOD_GENERATION_TOUCH_1:
160         case ITDB_IPOD_GENERATION_TOUCH_2:
161         case ITDB_IPOD_GENERATION_TOUCH_3:
162         case ITDB_IPOD_GENERATION_IPHONE_1:
163         case ITDB_IPOD_GENERATION_IPHONE_2:
164         case ITDB_IPOD_GENERATION_IPHONE_3:
165             return true; // ITDB_CHECKSUM_HASH72
166         default:
167             break;
168     }
169     return false;
170 }
171 
172 static bool
hashAbNeeded(const Itdb_IpodGeneration & generation)173 hashAbNeeded( const Itdb_IpodGeneration &generation )
174 {
175     switch( generation )
176     {
177         // taken from libgpod itdb_device.c itdb_device_get_checksum_type()
178         // TODO: not nice, new released devices may be added!
179         case ITDB_IPOD_GENERATION_IPAD_1:
180         case ITDB_IPOD_GENERATION_IPHONE_4:
181         case ITDB_IPOD_GENERATION_TOUCH_4:
182         case ITDB_IPOD_GENERATION_NANO_6:
183             return true; // ITDB_CHECKSUM_HASHAB
184         default:
185             break;
186     }
187     return false;
188 }
189 
190 /**
191  * Returns true if file @param relFilename is found, readable and nonempty.
192  * Searches in @param mountPoint /iPod_Control/Device/
193  */
194 static bool
fileFound(const QString & mountPoint,const QString & relFilename)195 fileFound( const QString &mountPoint, const QString &relFilename )
196 {
197     gchar *controlDir = itdb_get_device_dir( QFile::encodeName( mountPoint ) );
198     if( !controlDir )
199         return false;
200     QString absFilename = QStringLiteral( "%1/%2" ).arg( QFile::decodeName( controlDir ),
201                                                   relFilename );
202     g_free( controlDir );
203 
204     QFileInfo fileInfo( absFilename );
205     return fileInfo.isReadable() && fileInfo.size() > 0;
206 }
207 
208 static bool
safeToWriteWithMessage(const QString & mountPoint,const Itdb_iTunesDB * itdb,QString & message)209 safeToWriteWithMessage( const QString &mountPoint, const Itdb_iTunesDB *itdb, QString &message )
210 {
211     const Itdb_IpodInfo *info = getIpodInfo( itdb ); // returns null on null itdb
212     if( !info )
213     {
214         message = i18n( "iPod model was not recognized." );
215         return false;
216     }
217 
218     QString gen = QString::fromUtf8( itdb_info_get_ipod_generation_string( info->ipod_generation ) );
219     if( firewireGuidNeeded( info->ipod_generation ) )
220     {
221         // okay FireWireGUID may be in plain SysInfo, too, but it's hard to check and
222         // error-prone so we just require SysInfoExtended which is machine-generated
223         const QString sysInfoExtended( "SysInfoExtended" );
224         bool sysInfoExtendedExists = fileFound( mountPoint, sysInfoExtended );
225         message += ( sysInfoExtendedExists )
226                    ? i18n( "%1 family uses %2 file to generate correct database checksum.",
227                            gen, sysInfoExtended )
228                    : i18n( "%1 family needs %2 file to generate correct database checksum.",
229                            gen, sysInfoExtended );
230         if( !sysInfoExtendedExists )
231             return false;
232     }
233     if( hashInfoNeeded( info->ipod_generation ) )
234     {
235         const QString hashInfo( "HashInfo" );
236         bool hashInfoExists = fileFound( mountPoint, hashInfo );
237         message += hashInfoExists
238                    ? i18n( "%1 family uses %2 file to generate correct database checksum.",
239                            gen, hashInfo )
240                    : i18n( "%1 family needs %2 file to generate correct database checksum.",
241                            gen, hashInfo );
242         if( !hashInfoExists )
243             return false;
244     }
245     if( hashAbNeeded( info->ipod_generation ) )
246     {
247         message += i18nc( "Do not translate hash-AB, libgpod, libhashab.so",
248             "%1 family probably uses hash-AB to generate correct database checksum. "
249             "libgpod (as of version 0.8.2) doesn't know how to compute it, but tries "
250             "to dynamically load external library libhashab.so to do it.", gen
251         );
252         // we don't return false, user may have hash-AB support installed
253     }
254     return true;
255 }
256 
257 static void
fillInModelComboBox(QComboBox * comboBox,bool someSysInfoFound)258 fillInModelComboBox( QComboBox *comboBox, bool someSysInfoFound )
259 {
260     if( someSysInfoFound )
261     {
262         comboBox->addItem( i18n( "Autodetect (%1 file(s) present)", QString( "SysInfo") ), QString() );
263         comboBox->setEnabled( false );
264         return;
265     }
266 
267     const Itdb_IpodInfo *info = itdb_info_get_ipod_info_table();
268     if( !info )
269     {
270         // this is not i18n-ed for purpose: it should never happen
271         comboBox->addItem( QStringLiteral( "Failed to get iPod info table!" ), QString() );
272         return;
273     }
274 
275     while( info->model_number )
276     {
277         QString generation = QString::fromUtf8( itdb_info_get_ipod_generation_string( info->ipod_generation) );
278         QString capacity = KFormat().formatByteSize( info->capacity * 1073741824.0, 0 );
279         QString modelName = QString::fromUtf8( itdb_info_get_ipod_model_name_string( info->ipod_model ) );
280         QString modelNumber = QString::fromUtf8( info->model_number );
281         QString label = i18nc( "Examples: "
282                                "%1: Nano with camera (5th Gen.); [generation]"
283                                "%2: 16 GiB; [capacity]"
284                                "%3: Nano (Orange); [model name]"
285                                "%4: A123 [model number]",
286                                "%1: %2 %3 [%4]",
287                                generation, capacity, modelName, modelNumber );
288         comboBox->addItem( label, modelNumber );
289         info++; // list is ended by null-filled info
290     }
291     comboBox->setMaxVisibleItems( 16 );
292 }
293 
294 void
fillInConfigureDialog(QDialog * configureDialog,Ui::IpodConfiguration * configureDialogUi,const QString & mountPoint,Itdb_iTunesDB * itdb,const Transcoding::Configuration & transcodeConfig,const QString & errorMessage)295 IpodDeviceHelper::fillInConfigureDialog( QDialog *configureDialog,
296                                          Ui::IpodConfiguration *configureDialogUi,
297                                          const QString &mountPoint,
298                                          Itdb_iTunesDB *itdb,
299                                          const Transcoding::Configuration &transcodeConfig,
300                                          const QString &errorMessage )
301 {
302     static const QString unknown = i18nc( "Unknown iPod model, generation...", "Unknown" );
303     static const QString supported = i18nc( "In a dialog: Video: Supported", "Supported" );
304     static const QString notSupported = i18nc( "In a dialog: Video: Not supported", "Not supported" );
305     static const QString present = i18nc( "In a dialog: Some file: Present", "Present" );
306     static const QString notFound = i18nc( "In a dialog: Some file: Not found", "<b>Not found</b>" );
307     static const QString notNeeded = i18nc( "In a dialog: Some file: Not needed", "Not needed" );
308 
309     // following call accepts null itdb
310     configureDialogUi->nameLineEdit->setText( IpodDeviceHelper::ipodName( itdb ) );
311     QString notes;
312     QString warningText;
313     QString safeToWriteMessage;
314     bool isSafeToWrite = safeToWriteWithMessage( mountPoint, itdb, safeToWriteMessage );
315     bool sysInfoExtendedExists = fileFound( mountPoint, "SysInfoExtended" );
316     bool sysInfoExists = fileFound( mountPoint, "SysInfo" );
317 
318     if( itdb )
319     {
320         configureDialogUi->nameLineEdit->setEnabled( isSafeToWrite );
321         configureDialogUi->transcodeComboBox->setEnabled( isSafeToWrite );
322         configureDialogUi->transcodeComboBox->fillInChoices( transcodeConfig );
323         configureDialogUi->modelComboLabel->setEnabled( false );
324         configureDialogUi->modelComboBox->setEnabled( false );
325         configureDialogUi->initializeLabel->setEnabled( false );
326         configureDialogUi->initializeButton->setEnabled( false );
327         if( !errorMessage.isEmpty() )
328             // to inform user about successful initialization.
329             warningText = QString( "<b>%1</b>" ).arg( errorMessage );
330 
331         const Itdb_Device *device = itdb->device;
332         const Itdb_IpodInfo *info = device ? itdb_device_get_ipod_info( device ) : 0;
333         configureDialogUi->infoGroupBox->setEnabled( true );
334         configureDialogUi->modelPlaceholer->setText( info ? QString::fromUtf8(
335             itdb_info_get_ipod_model_name_string( info->ipod_model ) ) : unknown );
336         configureDialogUi->generationPlaceholder->setText( info ? QString::fromUtf8(
337             itdb_info_get_ipod_generation_string( info->ipod_generation ) ) : unknown );
338         configureDialogUi->videoPlaceholder->setText( device ?
339             ( itdb_device_supports_video( device ) ? supported : notSupported ) : unknown );
340         configureDialogUi->albumArtworkPlaceholder->setText( device ?
341             ( itdb_device_supports_artwork( device ) ? supported : notSupported ) : unknown );
342 
343         if( isSafeToWrite )
344             notes += safeToWriteMessage; // may be empty, doesn't hurt
345         else
346         {
347             Q_ASSERT( !safeToWriteMessage.isEmpty() );
348             const QString link( "http://gtkpod.git.sourceforge.net/git/gitweb.cgi?p=gtkpod/libgpod;a=blob_plain;f=README.overview" );
349             notes += i18nc( "%1 is informational sentence giving reason",
350                 "<b>%1</b><br><br>"
351                 "As a safety measure, Amarok will <i>refuse to perform any writes</i> to "
352                 "iPod. (modifying iTunes database could make it look empty from the device "
353                 "point of view)<br>"
354                 "See <a href='%2'>README.overview</a> file from libgpod source repository "
355                 "for more information.",
356                 safeToWriteMessage, link
357             );
358         }
359     }
360     else
361     {
362         configureDialogUi->nameLineEdit->setEnabled( true ); // for initialization
363         configureDialogUi->modelComboLabel->setEnabled( true );
364         configureDialogUi->modelComboBox->setEnabled( true );
365         if( configureDialogUi->modelComboBox->count() == 0 )
366             fillInModelComboBox( configureDialogUi->modelComboBox, sysInfoExists || sysInfoExtendedExists );
367         configureDialogUi->initializeLabel->setEnabled( true );
368         configureDialogUi->initializeButton->setEnabled( true );
369         configureDialogUi->initializeButton->setIcon( QIcon::fromTheme( "task-attention" ) );
370         if( !errorMessage.isEmpty() )
371             warningText = i18n(
372                 "<b>%1</b><br><br>"
373                 "Above problem prevents Amarok from using your iPod. You can try to "
374                 "re-create critical iPod folders and files (including iTunes database) "
375                 "using the <b>%2</b> button below.<br><br> "
376                 "Initializing iPod <b>destroys iPod track and photo database</b>, however "
377                 "it should not delete any tracks. The tracks will become orphaned.",
378                 errorMessage,
379                 configureDialogUi->initializeButton->text().remove( QChar('&') )
380             );
381 
382         configureDialogUi->infoGroupBox->setEnabled( false );
383         configureDialogUi->modelPlaceholer->setText(  unknown );
384         configureDialogUi->generationPlaceholder->setText(  unknown );
385         configureDialogUi->videoPlaceholder->setText(  unknown );
386         configureDialogUi->albumArtworkPlaceholder->setText( unknown );
387     }
388 
389     if( !warningText.isEmpty() )
390     {
391         configureDialogUi->initializeLabel->setText( warningText );
392         configureDialogUi->initializeLabel->adjustSize();
393     }
394 
395     QString sysInfoExtendedString = sysInfoExtendedExists ? present : notFound;
396     QString sysInfoString = sysInfoExists ? present :
397                           ( sysInfoExtendedExists ? notNeeded : notFound );
398 
399     configureDialogUi->sysInfoPlaceholder->setText( sysInfoString );
400     configureDialogUi->sysInfoExtendedPlaceholder->setText( sysInfoExtendedString );
401     configureDialogUi->notesPlaceholder->setText( notes );
402     configureDialogUi->notesPlaceholder->adjustSize();
403 
404     configureDialog->findChild<QDialogButtonBox*>()->button( QDialogButtonBox::Ok )->setEnabled( isSafeToWrite );
405 }
406 
407 bool
initializeIpod(const QString & mountPoint,const Ui::IpodConfiguration * configureDialogUi,QString & errorMessage)408 IpodDeviceHelper::initializeIpod( const QString &mountPoint,
409                                   const Ui::IpodConfiguration *configureDialogUi,
410                                   QString &errorMessage )
411 {
412     DEBUG_BLOCK
413     bool success = true;
414 
415     int currentModelIndex = configureDialogUi->modelComboBox->currentIndex();
416     QByteArray modelNumber = configureDialogUi->modelComboBox->itemData( currentModelIndex ).toString().toUtf8();
417     if( !modelNumber.isEmpty() )
418     {
419         modelNumber.prepend( 'x' );  // ModelNumStr should start with x
420         const char *modelNumberRaw = modelNumber.constData();
421         Itdb_Device *device = itdb_device_new();
422         // following call reads existing SysInfo
423         itdb_device_set_mountpoint( device, QFile::encodeName( mountPoint ) );
424         const char *field = "ModelNumStr";
425         debug() << "Setting SysInfo field" << field << "to value" << modelNumberRaw;
426         itdb_device_set_sysinfo( device, field, modelNumberRaw );
427         GError *error = 0;
428         success = itdb_device_write_sysinfo( device, &error );
429         if( !success )
430         {
431             if( error )
432             {
433                 errorMessage = i18nc( "Do not translate SysInfo",
434                                       "Failed to write SysInfo: %1", error->message );
435                 g_error_free( error );
436             }
437             else
438                 errorMessage = i18nc( "Do not translate SysInfo",
439                     "Failed to write SysInfo file due to an unreported error" );
440         }
441         itdb_device_free( device );
442         if( !success )
443             return success;
444     }
445 
446     QString name = configureDialogUi->nameLineEdit->text();
447     if( name.isEmpty() )
448         name = ipodName( 0 ); // return fallback name
449 
450     GError *error = 0;
451     success = itdb_init_ipod( QFile::encodeName( mountPoint ), 0 /* model number */,
452                               name.toUtf8(), &error );
453     errorMessage.clear();
454     if( error )
455     {
456         errorMessage = QString::fromUtf8( error->message );
457         g_error_free( error );
458         error = 0;
459     }
460     if( !success && errorMessage.isEmpty() )
461         errorMessage = i18n( "Cannot initialize iPod due to an unreported error." );
462     return success;
463 }
464 
465 void
setIpodName(Itdb_iTunesDB * itdb,const QString & newName)466 IpodDeviceHelper::setIpodName( Itdb_iTunesDB *itdb, const QString &newName )
467 {
468     if( !itdb )
469         return;
470     Itdb_Playlist *mpl = itdb_playlist_mpl( itdb );
471     if( !mpl )
472         return;
473     g_free( mpl->name );
474     mpl->name = g_strdup( newName.toUtf8() );
475 }
476 
477 bool
safeToWrite(const QString & mountPoint,const Itdb_iTunesDB * itdb)478 IpodDeviceHelper::safeToWrite( const QString &mountPoint, const Itdb_iTunesDB *itdb )
479 {
480     QString dummyMessage;
481     return safeToWriteWithMessage( mountPoint, itdb, dummyMessage );
482 }
483