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