5
// Aaron Bockover <abockover@novell.com>
7
// Copyright (C) 2005-2008 Novell, Inc.
9
// Permission is hereby granted, free of charge, to any person obtaining
10
// a copy of this software and associated documentation files (the
11
// "Software"), to deal in the Software without restriction, including
12
// without limitation the rights to use, copy, modify, merge, publish,
13
// distribute, sublicense, and/or sell copies of the Software, and to
14
// permit persons to whom the Software is furnished to do so, subject to
15
// the following conditions:
17
// The above copyright notice and this permission notice shall be
18
// included in all copies or substantial portions of the Software.
20
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
using System.Threading;
32
using System.Collections.Generic;
40
using Banshee.ServiceStack;
41
using Banshee.Sources;
43
using Banshee.Hardware;
44
using Banshee.Collection.Database;
45
using Banshee.Library;
46
using Banshee.Playlist;
48
using Banshee.Dap.Gui;
50
namespace Banshee.Dap.Ipod
52
public class IpodSource : DapSource
54
private PodSleuthDevice ipod_device;
55
internal PodSleuthDevice IpodDevice {
56
get { return ipod_device; }
59
private Dictionary<int, IpodTrackInfo> tracks_map = new Dictionary<int, IpodTrackInfo> (); // FIXME: EPIC FAIL
60
private bool database_loaded;
62
private string name_path;
63
internal string NamePath {
64
get { return name_path; }
67
private string music_path;
69
private bool database_supported;
70
internal bool DatabaseSupported {
71
get { return database_supported; }
74
private UnsupportedDatabaseView unsupported_view;
76
#region Device Setup/Dispose
78
public override void DeviceInitialize (IDevice device)
80
base.DeviceInitialize (device);
82
ipod_device = device as PodSleuthDevice;
83
if (ipod_device == null) {
84
throw new InvalidDeviceException ();
87
name_path = Path.Combine (Path.GetDirectoryName (ipod_device.TrackDatabasePath), "BansheeIPodName");
88
music_path = Path.Combine (ipod_device.ControlPath, "Music");
89
Name = GetDeviceName ();
91
SupportsPlaylists = ipod_device.ModelInfo.DeviceClass != "shuffle";
93
// TODO disable this later, but right now it won't disable it in Sync, so might as well
95
SupportsPodcasts = ipod_device.ModelInfo.HasCapability ("podcast");
96
SupportsVideo = ipod_device.ModelInfo.DeviceClass == "video" ||
97
ipod_device.ModelInfo.DeviceClass == "classic" ||
98
(ipod_device.ModelInfo.DeviceClass == "nano" && ipod_device.ModelInfo.Generation >= 3);
102
AddDapProperty (Catalog.GetString ("Device"), ipod_device.ModelInfo.DeviceClass);
103
AddDapProperty (Catalog.GetString ("Color"), ipod_device.ModelInfo.ShellColor);
104
AddDapProperty (Catalog.GetString ("Generation"), ipod_device.ModelInfo.Generation.ToString ());
105
AddDapProperty (Catalog.GetString ("Capacity"), ipod_device.ModelInfo.AdvertisedCapacity);
106
AddDapProperty (Catalog.GetString ("Serial number"), ipod_device.Serial);
107
AddDapProperty (Catalog.GetString ("Produced on"), ipod_device.ProductionInfo.DisplayDate);
108
AddDapProperty (Catalog.GetString ("Firmware"), ipod_device.FirmwareVersion);
110
string [] capabilities = new string [ipod_device.ModelInfo.Capabilities.Count];
111
ipod_device.ModelInfo.Capabilities.CopyTo (capabilities, 0);
112
AddDapProperty (Catalog.GetString ("Capabilities"), String.Join (", ", capabilities));
113
AddYesNoDapProperty (Catalog.GetString ("Supports cover art"), ipod_device.ModelInfo.AlbumArtSupported);
114
AddYesNoDapProperty (Catalog.GetString ("Supports photos"), ipod_device.ModelInfo.PhotosSupported);
117
public override void Dispose ()
119
ThreadAssist.ProxyToMain (DestroyUnsupportedView);
124
// WARNING: This will be called from a thread!
125
protected override void Eject ()
129
if (ipod_device.CanUnmount) {
130
ipod_device.Unmount ();
133
if (ipod_device.CanEject) {
134
ipod_device.Eject ();
140
protected override bool CanHandleDeviceCommand (DeviceCommand command)
143
SafeUri uri = new SafeUri (command.DeviceId);
144
return IpodDevice.MountPoint.StartsWith (uri.LocalPath);
150
protected override IDeviceMediaCapabilities MediaCapabilities {
151
get { return ipod_device.Parent.MediaCapabilities ?? base.MediaCapabilities; }
156
#region Database Loading
158
// WARNING: This will be called from a thread!
159
protected override void LoadFromDevice ()
162
LoadFromDevice (false);
166
private void LoadIpod ()
168
database_supported = false;
171
if (File.Exists (ipod_device.TrackDatabasePath)) {
172
ipod_device.LoadTrackDatabase (false);
174
int count = CountMusicFiles ();
175
Log.DebugFormat ("Found {0} files in /iPod_Control/Music", count);
176
if (CountMusicFiles () > 5) {
177
throw new DatabaseReadException ("No database, but found a lot of music files");
180
database_supported = true;
181
ThreadAssist.ProxyToMain (DestroyUnsupportedView);
182
} catch (DatabaseReadException e) {
183
Log.Exception ("Could not read iPod database", e);
184
ipod_device.LoadTrackDatabase (true);
186
ThreadAssist.ProxyToMain (delegate {
187
DestroyUnsupportedView ();
188
unsupported_view = new UnsupportedDatabaseView (this);
189
unsupported_view.Refresh += OnRebuildDatabaseRefresh;
190
Properties.Set<Banshee.Sources.Gui.ISourceContents> ("Nereid.SourceContents", unsupported_view);
192
} catch (Exception e) {
196
database_loaded = true;
198
Name = GetDeviceName ();
201
private int CountMusicFiles ()
206
DirectoryInfo m_dir = new DirectoryInfo (music_path);
207
foreach (DirectoryInfo f_dir in m_dir.GetDirectories ()) {
208
file_count += f_dir.GetFiles().Length;
217
private void LoadFromDevice (bool refresh)
219
// bool previous_database_supported = database_supported;
222
ipod_device.TrackDatabase.Reload ();
227
if (database_supported || (ipod_device.HasTrackDatabase &&
228
ipod_device.ModelInfo.DeviceClass == "shuffle")) {
229
foreach (Track ipod_track in ipod_device.TrackDatabase.Tracks) {
231
IpodTrackInfo track = new IpodTrackInfo (ipod_track);
232
track.PrimarySource = this;
234
tracks_map.Add (track.TrackId, track);
235
} catch (Exception e) {
240
Hyena.Data.Sqlite.HyenaSqliteCommand insert_cmd = new Hyena.Data.Sqlite.HyenaSqliteCommand (
241
@"INSERT INTO CorePlaylistEntries (PlaylistID, TrackID)
242
SELECT ?, TrackID FROM CoreTracks WHERE PrimarySourceID = ? AND ExternalID = ?");
243
foreach (IPod.Playlist playlist in ipod_device.TrackDatabase.Playlists) {
244
if (playlist.IsOnTheGo) { // || playlist.IsPodcast) {
247
PlaylistSource pl_src = new PlaylistSource (playlist.Name, this);
249
// We use the IPod.Track.Id here b/c we just shoved it into ExternalID above when we loaded
250
// the tracks, however when we sync, the Track.Id values may/will change.
251
foreach (IPod.Track track in playlist.Tracks) {
252
ServiceManager.DbConnection.Execute (insert_cmd, pl_src.DbId, this.DbId, track.Id);
254
pl_src.UpdateCounts ();
255
AddChildSource (pl_src);
260
BuildDatabaseUnsupportedWidget ();
263
/*if(previous_database_supported != database_supported) {
264
OnPropertiesChanged();
268
private void OnRebuildDatabaseRefresh (object o, EventArgs args)
270
ServiceManager.SourceManager.SetActiveSource (MusicGroupSource);
271
base.LoadDeviceContents ();
274
private void DestroyUnsupportedView ()
276
if (unsupported_view != null) {
277
unsupported_view.Refresh -= OnRebuildDatabaseRefresh;
278
unsupported_view.Destroy ();
279
unsupported_view = null;
285
#region Source Cosmetics
287
internal string [] _GetIconNames ()
289
return GetIconNames ();
292
protected override string [] GetIconNames ()
294
string [] names = new string[4];
295
string prefix = "multimedia-player-";
296
string shell_color = ipod_device.ModelInfo.ShellColor;
298
names[0] = ipod_device.ModelInfo.IconName;
299
names[2] = "ipod-standard-color";
300
names[3] = "multimedia-player";
302
switch (ipod_device.ModelInfo.DeviceClass) {
304
names[1] = "ipod-standard-monochrome";
307
names[1] = "ipod-standard-color";
310
names[1] = String.Format ("ipod-mini-{0}", shell_color);
311
names[2] = "ipod-mini-silver";
314
names[1] = String.Format ("ipod-shuffle-{0}", shell_color);
315
names[2] = "ipod-shuffle";
319
names[1] = String.Format ("ipod-nano-{0}", shell_color);
320
names[2] = "ipod-nano-white";
323
names[1] = String.Format ("ipod-video-{0}", shell_color);
324
names[2] = "ipod-video-white";
333
names[1] = names[1] ?? names[2];
334
names[1] = prefix + names[1];
335
names[2] = prefix + names[2];
340
public override void Rename (string name)
347
if (name_path != null) {
348
Directory.CreateDirectory (Path.GetDirectoryName (name_path));
350
using (StreamWriter writer = new StreamWriter (File.Open (name_path, FileMode.Create),
351
System.Text.Encoding.Unicode)) {
355
} catch (Exception e) {
359
ipod_device.Name = name;
363
private string GetDeviceName ()
366
if (File.Exists (name_path)) {
367
using (StreamReader reader = new StreamReader (name_path, System.Text.Encoding.Unicode)) {
368
name = reader.ReadLine ();
372
if (String.IsNullOrEmpty (name) && database_loaded && database_supported) {
373
name = ipod_device.Name;
376
if (!String.IsNullOrEmpty (name)) {
378
} else if (ipod_device.PropertyExists ("volume.label")) {
379
name = ipod_device.GetPropertyString ("volume.label");
380
} else if (ipod_device.PropertyExists ("info.product")) {
381
name = ipod_device.GetPropertyString ("info.product");
383
name = ((IDevice)ipod_device).Name ?? "iPod";
389
public override bool CanRename {
390
get { return !(IsAdding || IsDeleting || IsReadOnly); }
393
public override long BytesUsed {
394
get { return (long)ipod_device.VolumeInfo.SpaceUsed; }
397
public override long BytesCapacity {
398
get { return (long)ipod_device.VolumeInfo.Size; }
405
public override void UpdateMetadata (DatabaseTrackInfo track)
408
IpodTrackInfo ipod_track;
409
if (!tracks_map.TryGetValue (track.TrackId, out ipod_track)) {
413
ipod_track.UpdateInfo (track);
414
tracks_to_update.Enqueue (ipod_track);
418
protected override void OnTracksChanged (params QueryField[] fields)
420
if (tracks_to_update.Count > 0 && !Sync.Syncing) {
423
base.OnTracksChanged (fields);
426
protected override void OnTracksAdded ()
428
if (!IsAdding && tracks_to_add.Count > 0 && !Sync.Syncing) {
431
base.OnTracksAdded ();
434
protected override void OnTracksDeleted ()
436
if (!IsDeleting && tracks_to_remove.Count > 0 && !Sync.Syncing) {
439
base.OnTracksDeleted ();
442
private Queue<IpodTrackInfo> tracks_to_add = new Queue<IpodTrackInfo> ();
443
private Queue<IpodTrackInfo> tracks_to_update = new Queue<IpodTrackInfo> ();
444
private Queue<IpodTrackInfo> tracks_to_remove = new Queue<IpodTrackInfo> ();
446
private uint sync_timeout_id = 0;
447
private object sync_timeout_mutex = new object ();
448
private object sync_mutex = new object ();
449
private Thread sync_thread;
450
private AutoResetEvent sync_thread_wait;
451
private bool sync_thread_dispose = false;
453
public override bool IsReadOnly {
454
get { return ipod_device.IsReadOnly || !database_supported; }
457
public override void Import ()
459
Banshee.ServiceStack.ServiceManager.Get<LibraryImportManager> ().Enqueue (music_path);
462
/*public override void CopyTrackTo (DatabaseTrackInfo track, SafeUri uri, BatchUserJob job)
464
throw new Exception ("Copy to Library is not implemented for iPods yet");
467
protected override bool DeleteTrack (DatabaseTrackInfo track)
470
if (!tracks_map.ContainsKey (track.TrackId)) {
474
IpodTrackInfo ipod_track = tracks_map[track.TrackId];
475
if (ipod_track != null) {
476
tracks_to_remove.Enqueue (ipod_track);
483
protected override void AddTrackToDevice (DatabaseTrackInfo track, SafeUri fromUri)
486
if (track.PrimarySourceId == DbId) {
490
if (track.Duration.Equals (TimeSpan.Zero)) {
491
throw new Exception (Catalog.GetString ("Track duration is zero"));
494
var ipod_track = new IpodTrackInfo (track) {
496
PrimarySource = this,
499
tracks_to_add.Enqueue (ipod_track);
503
public override void SyncPlaylists ()
505
if (!IsReadOnly && Monitor.TryEnter (sync_mutex)) {
507
Monitor.Exit (sync_mutex);
511
private void QueueSync ()
513
lock (sync_timeout_mutex) {
514
if (sync_timeout_id > 0) {
515
Application.IdleTimeoutRemove (sync_timeout_id);
518
sync_timeout_id = Application.RunTimeout (150, PerformSync);
522
private void CancelSyncThread ()
524
Thread thread = sync_thread;
526
if (sync_thread != null && sync_thread_wait != null) {
527
sync_thread_dispose = true;
528
sync_thread_wait.Set ();
532
if (thread != null) {
537
private bool PerformSync ()
540
if (sync_thread == null) {
541
sync_thread_wait = new AutoResetEvent (false);
543
sync_thread = new Thread (new ThreadStart (PerformSyncThread));
544
sync_thread.Name = "iPod Sync Thread";
545
sync_thread.IsBackground = false;
546
sync_thread.Priority = ThreadPriority.Lowest;
547
sync_thread.Start ();
550
sync_thread_wait.Set ();
552
lock (sync_timeout_mutex) {
560
private void PerformSyncThread ()
564
sync_thread_wait.WaitOne ();
565
if (sync_thread_dispose) {
569
PerformSyncThreadCycle ();
573
sync_thread_dispose = false;
574
sync_thread_wait.Close ();
575
sync_thread_wait = null;
578
} catch (Exception e) {
583
private void PerformSyncThreadCycle ()
585
Hyena.Log.Debug ("Starting iPod sync thread cycle");
587
CreateNewSyncUserJob ();
589
var total = tracks_to_add.Count;
590
while (tracks_to_add.Count > 0) {
591
IpodTrackInfo track = null;
593
total = tracks_to_add.Count + i;
594
track = tracks_to_add.Dequeue ();
597
ChangeSyncProgress (track.ArtistName, track.TrackTitle, ++i / total);
600
track.CommitToIpod (ipod_device);
601
tracks_map[track.TrackId] = track;
603
} catch (Exception e) {
604
Log.Exception ("Cannot save track to iPod", e);
609
OnUserNotifyUpdated ();
612
while (tracks_to_update.Count > 0) {
613
IpodTrackInfo track = null;
615
track = tracks_to_update.Dequeue ();
619
track.CommitToIpod (ipod_device);
620
} catch (Exception e) {
621
Log.Exception ("Cannot save track to iPod", e);
625
while (tracks_to_remove.Count > 0) {
626
IpodTrackInfo track = null;
628
track = tracks_to_remove.Dequeue ();
631
if (tracks_map.ContainsKey (track.TrackId)) {
632
tracks_map.Remove (track.TrackId);
636
if (track.IpodTrack != null) {
637
ipod_device.TrackDatabase.RemoveTrack (track.IpodTrack);
639
} catch (Exception e) {
640
Log.Exception ("Cannot remove track from iPod", e);
644
// Remove playlists on the device
645
List<IPod.Playlist> device_playlists = new List<IPod.Playlist> (ipod_device.TrackDatabase.Playlists);
646
foreach (IPod.Playlist playlist in device_playlists) {
647
if (!playlist.IsOnTheGo) {
648
ipod_device.TrackDatabase.RemovePlaylist (playlist);
651
device_playlists.Clear ();
653
if (SupportsPlaylists) {
654
// Add playlists from Banshee to the device
655
List<Source> children = null;
657
children = new List<Source> (Children);
659
foreach (Source child in children) {
660
PlaylistSource from = child as PlaylistSource;
661
if (from != null && from.Count > 0) {
662
IPod.Playlist playlist = ipod_device.TrackDatabase.CreatePlaylist (from.Name);
663
foreach (int track_id in ServiceManager.DbConnection.QueryEnumerable<int> (String.Format (
664
"SELECT CoreTracks.TrackID FROM {0} WHERE {1}",
665
from.DatabaseTrackModel.ConditionFromFragment, from.DatabaseTrackModel.Condition)))
667
if (tracks_map.ContainsKey (track_id)) {
668
playlist.AddTrack (tracks_map[track_id].IpodTrack);
676
ipod_device.TrackDatabase.SaveEnded += OnIpodDatabaseSaveEnded;
677
ipod_device.TrackDatabase.SaveProgressChanged += OnIpodDatabaseSaveProgressChanged;
679
} catch (InsufficientSpaceException) {
680
ErrorSource.AddMessage (Catalog.GetString ("Out of space on device"), Catalog.GetString ("Please manually remove some songs"));
681
} catch (Exception e) {
682
Log.Exception ("Failed to save iPod database", e);
684
ipod_device.TrackDatabase.SaveEnded -= OnIpodDatabaseSaveEnded;
685
ipod_device.TrackDatabase.SaveProgressChanged -= OnIpodDatabaseSaveProgressChanged;
686
Hyena.Log.Debug ("Ending iPod sync thread cycle");
690
private UserJob sync_user_job;
692
private void CreateNewSyncUserJob ()
694
sync_user_job = new UserJob (Catalog.GetString ("Syncing iPod"),
695
Catalog.GetString ("Preparing to synchronize..."), GetIconNames ());
696
sync_user_job.Register ();
699
private void OnIpodDatabaseSaveEnded (object o, EventArgs args)
701
DisposeSyncUserJob ();
704
private void DisposeSyncUserJob ()
706
if (sync_user_job != null) {
707
sync_user_job.Finish ();
708
sync_user_job = null;
712
private void OnIpodDatabaseSaveProgressChanged (object o, IPod.TrackSaveProgressArgs args)
714
if (args.CurrentTrack == null) {
715
ChangeSyncProgress (null, null, 0.0);
717
ChangeSyncProgress (args.CurrentTrack.Artist, args.CurrentTrack.Title, args.TotalProgress);
721
private void ChangeSyncProgress (string artist, string title, double progress)
723
string message = (artist == null && title == null)
724
? Catalog.GetString ("Updating...")
725
: String.Format ("{0} - {1}", artist, title);
727
if (progress >= 0.99) {
728
sync_user_job.Status = Catalog.GetString ("Flushing to disk...");
729
sync_user_job.Progress = 0;
731
sync_user_job.Status = message;
732
sync_user_job.Progress = progress;
736
public bool SyncNeeded {
739
return tracks_to_add.Count > 0 ||
740
tracks_to_update.Count > 0 ||
741
tracks_to_remove.Count > 0;
747
public override bool HasEditableTrackProperties {
749
// we want child sources to be able to edit metadata and the
750
// savetrackmetadataservice to take in account this source