Index: ConfigurationForm.cs =================================================================== --- ConfigurationForm.cs (Revision 239) +++ ConfigurationForm.cs (Arbeitskopie) @@ -171,6 +171,8 @@ textDeviceName.Text = device.Name; textMediaRoot.Text = device.MediaRoot; textRecognizePattern.Text = device.RecognizePattern; + textExportPlaylist.Text = device.ExportPlaylist.File; + textExportPlaylist.Tag = (long)device.ExportPlaylist.Type; foreach (SyncPattern pattern in deviceConfiguration.SyncPatterns) { @@ -224,6 +226,7 @@ textMediaRoot.Enabled = true; textRecognizePattern.Enabled = true; comboAssociatePlaylist.Enabled = true; + textExportPlaylist.Enabled = true; if (!forNewDevice) buttonDelete.Enabled = true; @@ -231,6 +234,8 @@ buttonBrowseMediaRoot.Enabled = true; buttonCreateUniqueFile.Enabled = true; comboAssociatePlaylist.Enabled = true; + buttonBrowseExportPlaylist.Enabled = true; + buttonClearExportPlaylist.Enabled = true; } @@ -244,12 +249,15 @@ textMediaRoot.Enabled = false; textRecognizePattern.Enabled = false; comboAssociatePlaylist.Enabled = false; + textExportPlaylist.Enabled = false; buttonBrowseMediaRoot.Enabled = false; buttonCreateUniqueFile.Enabled = false; buttonDelete.Enabled = false; buttonSave.Enabled = false; comboAssociatePlaylist.Enabled = false; + buttonBrowseExportPlaylist.Enabled = false; + buttonClearExportPlaylist.Enabled = false; } private void comboSyncPatterns_SelectedIndexChanged(object sender, EventArgs e) @@ -294,6 +302,8 @@ textRecognizePattern.Text = ""; comboSyncPatterns.SelectedIndex = 0; comboAssociatePlaylist.SelectedIndex = 0; + textExportPlaylist.Text = ""; + textExportPlaylist.Tag = ""; } /// @@ -342,7 +352,13 @@ string mediaroot = textMediaRoot.Text; string recognizePattern = textRecognizePattern.Text; string associatedPlaylist = (string)comboAssociatePlaylist.SelectedItem; - + string exportplaylist = textExportPlaylist.Text; + long exportplaylisttype = 0; + if (textExportPlaylist.Tag.ToString().Length > 0) + { + exportplaylisttype = (long)textExportPlaylist.Tag; + } + if (deviceName.Length == 0) { MessageBox.Show(this, "Please enter a name for the device.", "Missing information", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); @@ -355,17 +371,31 @@ return; } + /*if (mediaroot.Length == 0) + { + MessageBox.Show(this, "Please enter a media folder for the device.", "Missing information", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + return; + }*/ + if (recognizePattern.Length == 0) { MessageBox.Show(this, "Please enter a recognize pattern for the device.", "Missing information", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); return; } + /*if (exportplaylist.Length == 0) + { + MessageBox.Show(this, "Please enter a export playlist for the device.", "Missing information", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + return; + }*/ + Device newDevice = new Device(); newDevice.Name = deviceName; newDevice.MediaRoot = mediaroot; newDevice.RecognizePattern = recognizePattern; newDevice.Playlist = (associatedPlaylist == "Use device name..." ? "" : associatedPlaylist); + newDevice.ExportPlaylist.File = exportplaylist; + newDevice.ExportPlaylist.Type = (ExportPlaylist.PlaylistTypes)exportplaylisttype; foreach (SyncPattern sp in deviceConfiguration.SyncPatterns) { if (sp.Name != syncPattern) @@ -402,6 +432,7 @@ device.RecognizePattern = newDevice.RecognizePattern; device.SyncPattern = newDevice.SyncPattern; device.Playlist = newDevice.Playlist; + device.ExportPlaylist = newDevice.ExportPlaylist; break; } @@ -455,7 +486,6 @@ SaveDeviceConfiguration(); } } - } /// @@ -604,6 +634,55 @@ configurationChanged = true; buttonOK.Enabled = true; } + private void buttonBrowseExportPlaylist_Click(object sender, EventArgs e) + { + //FolderBrowserDialog dlg = new FolderBrowserDialog(); + SaveFileDialog dlg = new SaveFileDialog(); + dlg.AddExtension = true; + dlg.Filter = "Basic M3U (*.m3u)|*.m3u|Extended M3U (*.m3u)|*.m3u|Windows Media (*.wpl)|*.wpl|Zune Playlist (*.zpl)|*.zpl"; + dlg.FilterIndex = 0; + //dlg.DefaultExt = "m3u"; + dlg.FileName = Path.GetFileName(textExportPlaylist.Text); + //dlg.InitialDirectory = Path.GetDirectoryName(textExportPlaylist.Text); + + if (dlg.ShowDialog() == DialogResult.Cancel) + return; + + //This check is to make sure that the application do not throw an exception if a path + //of length less than 3 is selected. Normally this do not occur, but there has been + //reportet incidents where unsupported, special devices have given an empty path in return... + //See bug report 1443246. + // https://sourceforge.net/tracker/index.php?func=detail&aid=1443246&group_id=149133&atid=773786 + if (dlg.FileName.Length >= 3) + { + textExportPlaylist.Text = dlg.FileName.Substring(3, dlg.FileName.Length - 3); + + switch (dlg.FilterIndex) + { + case 1: + textExportPlaylist.Tag = (long)ExportPlaylist.PlaylistTypes.M3U; + break; + case 2: + textExportPlaylist.Tag = (long)ExportPlaylist.PlaylistTypes.EXT; + break; + case 3: + textExportPlaylist.Tag = (long)ExportPlaylist.PlaylistTypes.WPL; + break; + case 4: + textExportPlaylist.Tag = (long)ExportPlaylist.PlaylistTypes.ZPL; + break; + default: + textExportPlaylist.Tag = (long)ExportPlaylist.PlaylistTypes.None; + break; + } + } + } + + private void buttonClearExportPlaylist_Click(object sender, EventArgs e) + { + textExportPlaylist.Text = ""; + textExportPlaylist.Tag = ""; + } } } \ No newline at end of file Index: ConfigurationForm.Designer.cs =================================================================== --- ConfigurationForm.Designer.cs (Revision 239) +++ ConfigurationForm.Designer.cs (Arbeitskopie) @@ -40,10 +40,14 @@ this.buttonBrowseMediaRoot = new System.Windows.Forms.Button(); this.comboSyncPatterns = new System.Windows.Forms.ComboBox(); this.buttonCreateUniqueFile = new System.Windows.Forms.Button(); + this.buttonBrowseExportPlaylist = new System.Windows.Forms.Button(); + this.buttonClearExportPlaylist = new System.Windows.Forms.Button(); this.textRecognizePattern = new System.Windows.Forms.TextBox(); this.textMediaRoot = new System.Windows.Forms.TextBox(); this.groupBox2 = new System.Windows.Forms.GroupBox(); this.groupDeviceInformation = new System.Windows.Forms.GroupBox(); + this.textExportPlaylist = new System.Windows.Forms.TextBox(); + this.label6 = new System.Windows.Forms.Label(); this.comboAssociatePlaylist = new System.Windows.Forms.ComboBox(); this.label5 = new System.Windows.Forms.Label(); this.buttonDelete = new System.Windows.Forms.Button(); @@ -71,9 +75,9 @@ this.groupBox1.Controls.Add(this.checkAutocloseSyncWindow); this.groupBox1.Controls.Add(this.checkUseListFolder); this.groupBox1.Controls.Add(this.checkNotifications); - this.groupBox1.Location = new System.Drawing.Point(2, 2); + this.groupBox1.Location = new System.Drawing.Point(2, 1); this.groupBox1.Name = "groupBox1"; - this.groupBox1.Size = new System.Drawing.Size(512, 96); + this.groupBox1.Size = new System.Drawing.Size(512, 97); this.groupBox1.TabIndex = 0; this.groupBox1.TabStop = false; this.groupBox1.Text = "Agent"; @@ -179,6 +183,30 @@ this.buttonCreateUniqueFile.UseVisualStyleBackColor = true; this.buttonCreateUniqueFile.Click += new System.EventHandler(this.buttonCreateUniqueFile_Click); // + // buttonBrowseExportPlaylist + // + this.buttonBrowseExportPlaylist.Enabled = false; + this.buttonBrowseExportPlaylist.Location = new System.Drawing.Point(358, 140); + this.buttonBrowseExportPlaylist.Name = "buttonBrowseExportPlaylist"; + this.buttonBrowseExportPlaylist.Size = new System.Drawing.Size(65, 23); + this.buttonBrowseExportPlaylist.TabIndex = 17; + this.buttonBrowseExportPlaylist.Text = "Choose"; + this.toolTip.SetToolTip(this.buttonBrowseExportPlaylist, "Browse for the folder if the device is already connected."); + this.buttonBrowseExportPlaylist.UseVisualStyleBackColor = true; + this.buttonBrowseExportPlaylist.Click += new System.EventHandler(this.buttonBrowseExportPlaylist_Click); + // + // buttonClearExportPlaylist + // + this.buttonClearExportPlaylist.Enabled = false; + this.buttonClearExportPlaylist.Location = new System.Drawing.Point(428, 140); + this.buttonClearExportPlaylist.Name = "buttonClearExportPlaylist"; + this.buttonClearExportPlaylist.Size = new System.Drawing.Size(65, 23); + this.buttonClearExportPlaylist.TabIndex = 18; + this.buttonClearExportPlaylist.Text = "Clear"; + this.toolTip.SetToolTip(this.buttonClearExportPlaylist, "Browse for the folder if the device is already connected."); + this.buttonClearExportPlaylist.UseVisualStyleBackColor = true; + this.buttonClearExportPlaylist.Click += new System.EventHandler(this.buttonClearExportPlaylist_Click); + // // textRecognizePattern // this.textRecognizePattern.Enabled = false; @@ -209,13 +237,17 @@ this.groupBox2.Controls.Add(this.listDevices); this.groupBox2.Location = new System.Drawing.Point(2, 104); this.groupBox2.Name = "groupBox2"; - this.groupBox2.Size = new System.Drawing.Size(512, 357); + this.groupBox2.Size = new System.Drawing.Size(512, 387); this.groupBox2.TabIndex = 1; this.groupBox2.TabStop = false; this.groupBox2.Text = "Devices"; // // groupDeviceInformation // + this.groupDeviceInformation.Controls.Add(this.buttonClearExportPlaylist); + this.groupDeviceInformation.Controls.Add(this.buttonBrowseExportPlaylist); + this.groupDeviceInformation.Controls.Add(this.textExportPlaylist); + this.groupDeviceInformation.Controls.Add(this.label6); this.groupDeviceInformation.Controls.Add(this.buttonCreateUniqueFile); this.groupDeviceInformation.Controls.Add(this.comboAssociatePlaylist); this.groupDeviceInformation.Controls.Add(this.label5); @@ -233,11 +265,28 @@ this.groupDeviceInformation.Controls.Add(this.label1); this.groupDeviceInformation.Location = new System.Drawing.Point(6, 182); this.groupDeviceInformation.Name = "groupDeviceInformation"; - this.groupDeviceInformation.Size = new System.Drawing.Size(500, 167); + this.groupDeviceInformation.Size = new System.Drawing.Size(500, 198); this.groupDeviceInformation.TabIndex = 1; this.groupDeviceInformation.TabStop = false; this.groupDeviceInformation.Text = "Device information"; // + // textExportPlaylist + // + this.textExportPlaylist.Enabled = false; + this.textExportPlaylist.Location = new System.Drawing.Point(140, 142); + this.textExportPlaylist.Name = "textExportPlaylist"; + this.textExportPlaylist.Size = new System.Drawing.Size(212, 20); + this.textExportPlaylist.TabIndex = 16; + // + // label6 + // + this.label6.AutoSize = true; + this.label6.Location = new System.Drawing.Point(6, 146); + this.label6.Name = "label6"; + this.label6.Size = new System.Drawing.Size(42, 13); + this.label6.TabIndex = 15; + this.label6.Text = "Playlist:"; + // // comboAssociatePlaylist // this.comboAssociatePlaylist.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; @@ -262,7 +311,7 @@ // buttonDelete // this.buttonDelete.Enabled = false; - this.buttonDelete.Location = new System.Drawing.Point(290, 138); + this.buttonDelete.Location = new System.Drawing.Point(290, 168); this.buttonDelete.Name = "buttonDelete"; this.buttonDelete.Size = new System.Drawing.Size(84, 23); this.buttonDelete.TabIndex = 11; @@ -273,7 +322,7 @@ // buttonSave // this.buttonSave.Enabled = false; - this.buttonSave.Location = new System.Drawing.Point(215, 138); + this.buttonSave.Location = new System.Drawing.Point(215, 168); this.buttonSave.Name = "buttonSave"; this.buttonSave.Size = new System.Drawing.Size(75, 23); this.buttonSave.TabIndex = 10; @@ -283,7 +332,7 @@ // // buttonNew // - this.buttonNew.Location = new System.Drawing.Point(140, 138); + this.buttonNew.Location = new System.Drawing.Point(140, 168); this.buttonNew.Name = "buttonNew"; this.buttonNew.Size = new System.Drawing.Size(75, 23); this.buttonNew.TabIndex = 9; @@ -360,7 +409,7 @@ // buttonCancel // this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.buttonCancel.Location = new System.Drawing.Point(439, 465); + this.buttonCancel.Location = new System.Drawing.Point(439, 497); this.buttonCancel.Name = "buttonCancel"; this.buttonCancel.Size = new System.Drawing.Size(75, 23); this.buttonCancel.TabIndex = 2; @@ -369,7 +418,7 @@ // buttonOK // this.buttonOK.Enabled = false; - this.buttonOK.Location = new System.Drawing.Point(2, 465); + this.buttonOK.Location = new System.Drawing.Point(2, 497); this.buttonOK.Name = "buttonOK"; this.buttonOK.Size = new System.Drawing.Size(75, 23); this.buttonOK.TabIndex = 3; @@ -381,7 +430,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.buttonCancel; - this.ClientSize = new System.Drawing.Size(518, 492); + this.ClientSize = new System.Drawing.Size(518, 525); this.Controls.Add(this.buttonOK); this.Controls.Add(this.buttonCancel); this.Controls.Add(this.groupBox2); @@ -435,5 +484,10 @@ private System.Windows.Forms.Button buttonCreateUniqueFile; private System.Windows.Forms.CheckBox checkWarnOnSystemDrives; private System.Windows.Forms.CheckBox checkConfirmMusicLocation; + + private System.Windows.Forms.Button buttonBrowseExportPlaylist; + private System.Windows.Forms.TextBox textExportPlaylist; + private System.Windows.Forms.Label label6; + private System.Windows.Forms.Button buttonClearExportPlaylist; } } \ No newline at end of file Index: ConfigurationForm.resx =================================================================== --- ConfigurationForm.resx (Revision 239) +++ ConfigurationForm.resx (Arbeitskopie) @@ -120,18 +120,12 @@ 17, 17 - - 17, 17 - 107, 17 ONLY playlists with a name matching the device name will be sorted into this folder. Dynamic playlists with a different name, or special playlists such as 'Party shuffle' will not be put in the 'My Devices' folder. - - 107, 17 - Index: ConnectedDevicesManagerImpl.cs =================================================================== --- ConnectedDevicesManagerImpl.cs (Revision 239) +++ ConnectedDevicesManagerImpl.cs (Arbeitskopie) @@ -4,6 +4,7 @@ using System.Text; using System.IO; using Jaranweb.iTunesAgent.Configuration12; +using System.Windows.Forms; namespace Jaranweb.iTunesAgent { @@ -59,7 +60,7 @@ IEnumerator keys = connectedDevices.Keys.GetEnumerator(); while (keys.MoveNext()) { - string driveLetter = (string)keys.Current; + string driveLetter = ((string)keys.Current).Substring(0, 3); bool found = false; Device device = (Device)connectedDevices[keys.Current]; @@ -67,11 +68,14 @@ //Loop through all drive info objects to look for the current drive. foreach (DriveInfo di in drives) { - Device recognized = RecognizeDevice(di); - if (di.Name == driveLetter && (recognized != null && recognized.Name == device.Name)) - { - found = true; - break; + //MessageBox.Show(di.RootDirectory + " - " + RecognizeDeviceCount(di), "", MessageBoxButtons.OK); + + for (int i = 0; i < RecognizeDeviceCount(di); i++) { + Device recognized = RecognizeDevice(di, i); + if (di.Name == driveLetter && (recognized != null && recognized.Name == device.Name)) { + found = true; + //break; + } } } @@ -95,16 +99,17 @@ //the defined patterns for devices that iTunes Agent recognizes. foreach (DriveInfo di in drives) { - Device recognized = RecognizeDevice(di); - if (recognized == null) - continue; + for (int i = 0; i < RecognizeDeviceCount(di); i++) { + Device recognized = RecognizeDevice(di, i); + if (recognized == null) + continue; - if (connectedDevices.ContainsKey(di.Name)) - continue; + if (connectedDevices.ContainsKey(di.Name + "_" + i)) + continue; - connectedDevices.Add(di.Name, recognized); - OnDeviceConnected(di, recognized); - + connectedDevices.Add(di.Name + "_" + i, recognized); + OnDeviceConnected(di, recognized); + } } } @@ -128,16 +133,64 @@ return null; } + private int RecognizeDeviceCount(DriveInfo drive) { + int i = 0; + + foreach (Device d in deviceConfig.Devices) { + if (Directory.Exists(drive.Name + d.RecognizePattern) || + File.Exists(drive.Name + d.RecognizePattern) || + drive.VolumeLabel == d.RecognizePattern) { + + i++; + } + } + + return i; + } + + private Device RecognizeDevice(DriveInfo drive, int index) { + int i = 0; + + foreach (Device d in deviceConfig.Devices) { + if (Directory.Exists(drive.Name + d.RecognizePattern) || + File.Exists(drive.Name + d.RecognizePattern) || + drive.VolumeLabel == d.RecognizePattern) { + + if (i == index) + return d; + + i++; + } + } + + return null; + } + /// /// Remove a device from the list of disconnected devices. /// /// The name of the drive where the device was connected. private void RemoveDevice(string drive) { - connectedDevices.Remove(drive); + IEnumerator keys = connectedDevices.Keys.GetEnumerator(); + while (keys.MoveNext()) + { + if (keys.Current.ToString().StartsWith(drive)) { + connectedDevices.Remove(keys.Current); + keys = connectedDevices.Keys.GetEnumerator(); + } + } } /// + /// Remove a device from the list of disconnected devices. + /// + /// The name of the drive where the device was connected. + private void RemoveDevice(string drive, int index) { + connectedDevices.Remove(drive + "_" + index); + } + + /// /// Method for sending out device disconnected events. /// /// The name of the drive where the device was located. Index: device-config.xml =================================================================== --- device-config.xml (Revision 239) +++ device-config.xml (Arbeitskopie) @@ -22,6 +22,13 @@ This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Artist - Name. + + playlist-order + Playlist-Order Folder + %TRACKIDXSPACE%%ARTIST% - %NAME% + This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Index Artist - Name. + + Index: Device.cs =================================================================== --- Device.cs (Revision 239) +++ Device.cs (Arbeitskopie) @@ -6,6 +6,34 @@ namespace Jaranweb.iTunesAgent.Configuration12 { + public class ExportPlaylist + { + + public enum PlaylistTypes { None = 0, M3U, EXT, WPL, ZPL }; + + private string file; + + private PlaylistTypes type; + + /// + /// Accessor for File. + /// + public string File + { + get { return file; } + set { file = value; } + } + + /// + /// Accessor for Type. + /// + public PlaylistTypes Type + { + get { return type; } + set { type = value; } + } + } + public class Device { private string name; @@ -18,6 +46,8 @@ private string playlist; + private ExportPlaylist exportPlaylist = new ExportPlaylist(); + private ArrayList initialTracks = new ArrayList(); /// @@ -66,6 +96,15 @@ } /// + /// Accessor for Export. + /// + public ExportPlaylist ExportPlaylist + { + get { return exportPlaylist; } + set { exportPlaylist = value; } + } + + /// /// Accessor for intialTracks. /// [XmlIgnore] Index: Docs/device-config.xml =================================================================== --- Docs/device-config.xml (Revision 239) +++ Docs/device-config.xml (Arbeitskopie) @@ -22,6 +22,13 @@ This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Artist - Name. + + playlist-order + Playlist-Order Folder + %TRACKIDXSPACE%%ARTIST% - %NAME% + This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Index Artist - Name. + + Index: IPlaylistWriter.cs =================================================================== --- IPlaylistWriter.cs (Revision 0) +++ IPlaylistWriter.cs (Revision 0) @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +using iTunesLib; + +namespace Jaranweb.iTunesAgent.PlaylistWriter +{ + /// + /// Provides common functionality to playlist writer implementations. + /// + abstract class IPlaylistWriter + { + protected string fileName; + protected string playlist; + + public IPlaylistWriter(string fileName, string playlist) + { + // Cleans up playlists with * characters in name. + this.fileName = fileName.Replace('*', '_'); + this.playlist = playlist; + } + + /// + /// Writes the passed track to the current playlist file. + /// + /// The display name details for the track. + /// Typed reference for the track, providing additional data. + public abstract void WriteTrack( string trackName, IITTrack track ); + + /// + /// Closes the file handle for the playlist being written. + /// + public abstract void Close(); + + /// + /// Deletes the current playlist file from disk. + /// + public void Delete() + { + new FileInfo(fileName).Delete(); + } + } +} Index: M3UExtPlaylistWriter.cs =================================================================== --- M3UExtPlaylistWriter.cs (Revision 0) +++ M3UExtPlaylistWriter.cs (Revision 0) @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using iTunesLib; + +namespace Jaranweb.iTunesAgent.PlaylistWriter +{ + /// + /// Creates an M3U playlist in the Extended format. This format is described at + /// these URLs: + /// http://hanna.pyxidis.org/tech/m3u.html + /// http://en.wikipedia.org/wiki/M3U + /// http://www.assistanttools.com/articles/m3u_playlist_format.shtml + /// + class M3UExtPlaylistWriter : IPlaylistWriter + { + private StreamWriter _writer; + private string _thePlaylist; + + private const string FILE_HEADER = "#EXTM3U"; + private const string LINE_FORMAT_STRING = "{0}{1}, {2} - {3}"; + private const string LINE_PREFIX = "#EXTINF:"; + + public M3UExtPlaylistWriter(string fileName, string playlist) : base(fileName, playlist) + { + _thePlaylist = playlist; + _writer = new StreamWriter(fileName, false, System.Text.Encoding.GetEncoding("utf-8")); + + /// File format requires a single line header + _writer.WriteLine(FILE_HEADER); + } + + public override void WriteTrack(string trackName, IITTrack track) + { + /// Each track has two lines in the playlist file. The first contains + /// display info (seconds, name), plus the path/filename. + _writer.WriteLine(string.Format(LINE_FORMAT_STRING, LINE_PREFIX, track.Time.ToString(), track.Artist, track.Name)); + _writer.WriteLine(trackName); + } + + public override void Close() + { + _writer.Close(); + _writer.Dispose(); + } + } +} Index: M3UPlaylistWriter.cs =================================================================== --- M3UPlaylistWriter.cs (Revision 0) +++ M3UPlaylistWriter.cs (Revision 0) @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using iTunesLib; + +namespace Jaranweb.iTunesAgent.PlaylistWriter +{ + /// + /// Generates basic M3U playlist files. + /// + class M3UPlaylistWriter : IPlaylistWriter + { + private StreamWriter writer; + + public M3UPlaylistWriter(String fileName, string playlist ) : base(fileName, playlist) + { + writer = new StreamWriter(fileName, false, System.Text.Encoding.GetEncoding("utf-8")); + } + + public override void WriteTrack(string trackName, IITTrack track) + { + writer.WriteLine( trackName ); + } + + public override void Close() + { + writer.Close(); + } + } +} Index: MainForm.cs =================================================================== --- MainForm.cs (Revision 239) +++ MainForm.cs (Arbeitskopie) @@ -40,6 +40,7 @@ private DeviceConfiguration deviceConfiguration; private Configuration configuration; + private ISynchronizer synchronizer; private ISynchronizeForm syncForm; private Hashtable deviceInitialPlaylists = new Hashtable(); @@ -482,8 +483,12 @@ return; } + // Create synchronizer and form. + synchronizer = new StandardSynchronizer(); syncForm = new StandardSynchronizerForm(); + synchronizer.Form = syncForm; syncForm.Show(); + Thread thread = new Thread(new ThreadStart(PerformSynchronize)); thread.Start(); } @@ -533,8 +538,6 @@ public void PerformSynchronize() { - - Hashtable deviceinfo = connectedDevices.GetConnectedDevicesWithDrives(); IEnumerator keys = deviceinfo.Keys.GetEnumerator(); try @@ -545,8 +548,7 @@ while (keys.MoveNext()) { - - string drive = (string)keys.Current; + string drive = ((string)keys.Current).Substring(0, 3); Device device = (Device)deviceinfo[keys.Current]; if (configuration.ConfirmMusicLocation && !GetMusicLocationConfirmation(drive + device.MediaRoot)) @@ -572,9 +574,7 @@ synchronizer.SynchronizeComplete += new SynchronizeCompleteEventHandler(OnSynchronizeComplete); synchronizer.SynchronizeCancelled += new SynchronizeCancelledEventHandler(OnSynchronizeCancelled); synchronizer.SynchronizeDevice((IITUserPlaylist)playlist, drive, device); - } - } catch (Exception ex) { @@ -597,7 +597,10 @@ } private void OnSynchronizeComplete(object sender) - { + { + //if (syncForm != null && configuration.CloseSyncWindowOnSuccess) + // syncForm.CloseSafe(); + if (configuration.ShowNotificationPopups) itaTray.ShowBalloonTip(5, "Synchronize complete", "Your device was successfully synchronized with iTunes.", ToolTipIcon.Info); } Index: Properties/Resources.Designer.cs =================================================================== --- Properties/Resources.Designer.cs (Revision 239) +++ Properties/Resources.Designer.cs (Arbeitskopie) @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// This code was generated by a tool. -// Runtime Version:2.0.50727.1433 +// Dieser Code wurde von einem Tool generiert. +// Laufzeitversion:4.0.30319.1 // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn +// der Code erneut generiert wird. // //------------------------------------------------------------------------------ @@ -13,13 +13,13 @@ /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert + // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. + // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen + // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -33,7 +33,7 @@ } /// - /// Returns the cached ResourceManager instance used by this class. + /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ } /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. + /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle + /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { @@ -68,14 +68,14 @@ } /// - /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> + /// Sucht eine lokalisierte Zeichenfolge, die <?xml version="1.0" encoding="utf-8"?> ///<DeviceConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> /// <SyncPatterns> /// <SyncPattern> /// <Identifier>itunes-style</Identifier> /// <Name>iTunes</Name> /// <Pattern>%ARTIST%\%ALBUM%\%TRACKNUMSPACE%%NAME%</Pattern> - /// <Description>This pattern places the tracks the same way iTunes places tracks in a managed iTunes music folder. Files are organized by Artist\Album\TrackNumber Name. This [rest of string was truncated]";. + /// <Description>This pattern places the tracks the same way iTunes places tracks in a managed iTunes music folder. Files are organized by Artist\Album\TrackNumber Name. This [Rest der Zeichenfolge wurde abgeschnitten]"; ähnelt. /// internal static string device_config { get { @@ -98,12 +98,14 @@ } /// - /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> + /// Sucht eine lokalisierte Zeichenfolge, die <?xml version="1.0" encoding="utf-8"?> ///<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> /// <ShowNotificationPopups>true</ShowNotificationPopups> /// <UseListFolder>true</UseListFolder> /// <CloseSyncWindowOnSuccess>true</CloseSyncWindowOnSuccess> - ///</Configuration>. + /// <WarnOnSystemDrives>true</WarnOnSystemDrives> + /// <ConfirmMusicLocation>true</ConfirmMusicLocation> + ///</Configuration> ähnelt. /// internal static string ita_config { get { @@ -119,10 +121,10 @@ } /// - /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8" ?> - ///<log4net> - /// <appender name="DebugFile" type="log4net.Appender.FileAppender"> - /// <file value="debug.log" /> + /// Sucht eine lokalisierte Zeichenfolge, die <?xml version="1.0" encoding="utf-8" ?> + ///<log4net> + /// <appender name="LogFile" type="log4net.Appender.FileAppender"> + /// <file value="ita.log" /> /// <appendToFile value="true" /> /// /// <layout type="log4net.Layout.PatternLayout"> @@ -131,10 +133,10 @@ /// </appender> /// /// <root> - /// <level value="DEBUG" /> - /// <appender-ref ref="DebugFile" /> + /// <level value="INFO" /> + /// <appender-ref ref="LogFile" /> /// </root> - ///</log4net>. + ///</log4net> ähnelt. /// internal static string logging { get { Index: Properties/Settings.Designer.cs =================================================================== --- Properties/Settings.Designer.cs (Revision 239) +++ Properties/Settings.Designer.cs (Arbeitskopie) @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// This code was generated by a tool. -// Runtime Version:2.0.50727.1433 +// Dieser Code wurde von einem Tool generiert. +// Laufzeitversion:4.0.30319.1 // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn +// der Code erneut generiert wird. // //------------------------------------------------------------------------------ @@ -12,7 +12,7 @@ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "9.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); Index: Resources/device-config.xml =================================================================== --- Resources/device-config.xml (Revision 239) +++ Resources/device-config.xml (Arbeitskopie) @@ -22,6 +22,13 @@ This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Artist - Name. + + playlist-order + Playlist-Order Folder + %TRACKIDXSPACE%%ARTIST% - %NAME% + This pattern organizes all tracks in the root media folder of the device. All tracks from all artists are in one folder and names like this: Index Artist - Name. + + Index: StandardSynchronizer.cs =================================================================== --- StandardSynchronizer.cs (Revision 239) +++ StandardSynchronizer.cs (Arbeitskopie) @@ -9,6 +9,7 @@ using System.Runtime.InteropServices; using System.Diagnostics; using Jaranweb.iTunesAgent.Configuration12; +using Jaranweb.iTunesAgent.PlaylistWriter; using System.Security.AccessControl; using log4net; @@ -131,10 +132,43 @@ string deviceMediaRoot = drive + (device.MediaRoot.Length > 0 ? device.MediaRoot + "\\" : ""); + string playlistFile = null; + if(device.ExportPlaylist.File.Length > 0) + playlistFile = drive + device.ExportPlaylist.File; + + IPlaylistWriter playlistWriter = null; + + if (File.Exists(playlistFile)) + { + File.Delete(playlistFile); + } + try { - foreach (IITTrack track in playlist.Tracks) + // Create a new instance of one of the IPlaylistWriter implementations to + // handle the file's contents. + switch (device.ExportPlaylist.Type) { + case ExportPlaylist.PlaylistTypes.M3U: + playlistWriter = new M3UPlaylistWriter(playlistFile, playlist.Name); + break; + case ExportPlaylist.PlaylistTypes.EXT: + playlistWriter = new M3UExtPlaylistWriter(playlistFile, playlist.Name); + break; + case ExportPlaylist.PlaylistTypes.WPL: + playlistWriter = new WPLPlaylistWriter(playlistFile, playlist.Name); + break; + case ExportPlaylist.PlaylistTypes.ZPL: + playlistWriter = new ZPLPlaylistWriter(playlistFile, playlist.Name); + break; + default: + break; + } + + for (int i = 1; i <= playlist.Tracks.Count ; i++) + { + IITTrack track = playlist.Tracks.get_ItemByPlayOrder(i); + if (syncForm.GetOperationCancelled()) { syncForm.SetCurrentStatus("Synchronization cancelled. 0 tracks added, 0 tracks removed."); @@ -156,7 +190,7 @@ try { - pathOnDevice = SyncPatternTranslator.Translate(devicePattern, (IITFileOrCDTrack)addTrack); + pathOnDevice = SyncPatternTranslator.Translate(devicePattern, (IITFileOrCDTrack)addTrack, i); } catch (Exception ex) { @@ -305,7 +339,7 @@ syncForm.SetMaxProgressValue(syncList.Count); syncForm.SetProgressValue(0); - //Check for new track in the playlist which should be copied to the device + // Check for new track in the playlist which should be copied to the device // NEW foreach: traverse synchronization list instead of playlist // Thanks to Robert Grabowski. foreach (string filePath in syncList.Keys) @@ -328,11 +362,30 @@ //Increase progress bar syncForm.SetProgressValue(syncForm.GetProgressValue() + 1); + //Continue with next track if it is not of a supported extension. + //if (file.Extension != ".mp3" && file.Extension != ".acc" && file.Extension != ".m4p" && file.Extension != ".m4a") + if (!extensions.Contains(Path.GetExtension(((IITFileOrCDTrack)track).Location))) + continue; + string trackPath = filePath.Substring(deviceMediaRoot.Length); // hack: cut out media root l.Debug("Checking for copy: " + filePath); + string trackRelativePath = ""; + if (playlistFile != null) + trackRelativePath = EvaluateRelativePath(Path.GetDirectoryName(playlistFile), filePath); + + if (trackRelativePath.StartsWith(".\\")) + trackRelativePath = trackRelativePath.Substring(2); + if (File.Exists(filePath)) + { + if (playlistWriter != null) + playlistWriter.WriteTrack(trackRelativePath, track); + continue; + } try { @@ -342,8 +395,10 @@ File.Copy(((IITFileOrCDTrack)track).Location, filePath, true); File.SetAttributes(filePath, FileAttributes.Normal); + + if (playlistWriter != null) + playlistWriter.WriteTrack(trackRelativePath, track); - syncForm.AddLogText(filePath + " copied successfully.", Color.Green); l.Debug("Copied: " + filePath); @@ -362,9 +417,7 @@ tracksAdded++; } - } - catch (MissingTrackException ex) - { + } catch (MissingTrackException ex) { syncForm.SetCurrentStatus(""); String message = "You have a missing file in your library. Please remove the track '" + ex.Track.Artist + " - " + ex.Track.Name + "' and try again. I am sorry for the inconvenience."; syncForm.AddLogText(message, Color.Red); @@ -375,9 +428,7 @@ l.Error(message, ex); return; - } - catch (Exception ex) - { + } catch (Exception ex) { string message = "An error occured while copying new tracks: " + ex.Message; syncForm.SetCurrentStatus(""); syncForm.AddLogText(message, @@ -391,6 +442,12 @@ return; } + if (playlistWriter != null) + { + playlistWriter.Close(); + playlistWriter = null; + } + syncForm.SetCurrentStatus("Synchronization completed. " + tracksAdded + " track(s) added, " + tracksRemoved + " track(s) removed."); syncForm.AddLogText("Completed. " + tracksAdded + " track(s) copied to your device.", Color.Green); @@ -401,6 +458,58 @@ /// + /// Resolve the relative path of a file to another file + /// + /// The path of the file, which is absolute. + /// The path of the file, which should be made relative. + private static string EvaluateRelativePath(string mainDirPath, string absoluteFilePath) + { + string[] firstPathParts = mainDirPath.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); + + string[] secondPathParts = absoluteFilePath.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar); + + int sameCounter = 0; + + for (int i = 0; i < Math.Min(firstPathParts.Length, secondPathParts.Length); i++) + { + if (!firstPathParts[i].ToLower().Equals(secondPathParts[i].ToLower())) + { + break; + } + sameCounter++; + } + + if (sameCounter == 0) + { + return absoluteFilePath; + } + + string newPath = String.Empty; + + for (int i = sameCounter; i < firstPathParts.Length; i++) + { + if (i > sameCounter) + { + newPath += Path.DirectorySeparatorChar; + } + newPath += ".."; + } + + if (newPath.Length == 0) + { + newPath = "."; + } + + for (int i = sameCounter; i < secondPathParts.Length; i++) + { + newPath += Path.DirectorySeparatorChar; + newPath += secondPathParts[i]; + } + + return newPath; + } + + /// /// Check if the necessary folders for the given track exists. If not, create them. /// /// The path of the track, relative to the device media root. Index: SyncPatternTranslator.cs =================================================================== --- SyncPatternTranslator.cs (Revision 239) +++ SyncPatternTranslator.cs (Arbeitskopie) @@ -21,11 +21,13 @@ /// %NAME% = The track name /// %TRACKNUMSPACE% = The track number with a trailing space /// %TRACKNUM% = The track number (no trailing space) + /// %TRACKIDXSPACE% = The track index with a trailing space + /// %TRACKIDX% = The track index (no trailing space) /// /// SyncPattern to translate. /// iTunes track containing track information. /// A string representation of pattern and track. - public static string Translate(SyncPattern pattern, IITFileOrCDTrack track) + public static string Translate(SyncPattern pattern, IITFileOrCDTrack track, long lTrackIndex) { try { @@ -44,6 +46,7 @@ patternstring = TranslateAlbum(track.Album, patternstring); patternstring = TranslateName(track, patternstring); patternstring = TranslateTrackNumber(track, patternstring); + patternstring = TranslateTrackIndex(track, patternstring, lTrackIndex); patternstring = TranslateExtension(track, patternstring); l.Debug("patternstring=" + patternstring); @@ -121,6 +124,29 @@ return patternstring; } + private static string TranslateTrackIndex(IITFileOrCDTrack track, string patternstring, long lTrackIndex) + { + //Replace track number with a number only if the index is set + if (lTrackIndex != 0) + { + //%TRACKIDXSPACE% + patternstring = patternstring.Replace("%TRACKIDXSPACE%", + lTrackIndex.ToString("000") + " "); + + //%TRACKIDX% + patternstring = patternstring.Replace("%TRACKIDX%", + lTrackIndex.ToString("000")); + } + else //If there is no track index set for the track + { + //%TRACKIDXSPACE% + patternstring = patternstring.Replace("%TRACKIDXSPACE%", ""); + //%TRACKIDX% + patternstring = patternstring.Replace("%TRACKIDX%", ""); + } + return patternstring; + } + /// /// Translate track album. /// Index: SyncPatternTranslatorTest.cs =================================================================== --- SyncPatternTranslatorTest.cs (Revision 239) +++ SyncPatternTranslatorTest.cs (Arbeitskopie) @@ -26,7 +26,7 @@ track.Location = "C:\\Music\\Coldplay\\X&Y\\Fix You.mp3"; Assert.AreEqual("Coldplay\\X&Y\\Fix You.mp3", - SyncPatternTranslator.Translate(pattern, track)); + SyncPatternTranslator.Translate(pattern, track, 0)); } } Index: WPLPlaylistWriter.cs =================================================================== --- WPLPlaylistWriter.cs (Revision 0) +++ WPLPlaylistWriter.cs (Revision 0) @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Xml; + +using iTunesLib; + +namespace Jaranweb.iTunesAgent.PlaylistWriter +{ + /// + /// Generates playlist files in the WPL playlist format used by Windows Media Player. + /// + class WPLPlaylistWriter : IPlaylistWriter + { + private XmlWriter xmlWriter; + + public WPLPlaylistWriter(string fileName, string playlist) : base(fileName, playlist) + { + xmlWriter = new XmlTextWriter(fileName, System.Text.Encoding.GetEncoding("utf-8")); + + xmlWriter.WriteRaw("\n"); + xmlWriter.WriteStartElement("smil"); + xmlWriter.WriteStartElement("head"); + xmlWriter.WriteElementString("author", ""); + xmlWriter.WriteElementString("title", playlist); + xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("body"); + xmlWriter.WriteStartElement("seq"); + } + + public override void WriteTrack(string trackName, IITTrack track) + { + xmlWriter.WriteStartElement("media"); + xmlWriter.WriteAttributeString("src", trackName); + xmlWriter.WriteEndElement(); + } + + public override void Close() + { + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + xmlWriter.Close(); + } + } +} Index: ZPLPlaylistWriter.cs =================================================================== --- ZPLPlaylistWriter.cs (Revision 0) +++ ZPLPlaylistWriter.cs (Revision 0) @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Xml; + +using iTunesLib; + +namespace Jaranweb.iTunesAgent.PlaylistWriter +{ + /// + /// Generates playlist files in the ZPL playlist format used by Microsoft Zune Media Player. + /// + class ZPLPlaylistWriter : IPlaylistWriter + { + private XmlWriter xmlWriter; + + public ZPLPlaylistWriter(string fileName, string playlist) : base(fileName, playlist) + { + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.OmitXmlDeclaration = true; + settings.Encoding = System.Text.Encoding.GetEncoding("utf-8"); + + xmlWriter = XmlWriter.Create(fileName, settings); + + xmlWriter.WriteRaw("\n"); + xmlWriter.WriteStartElement("smil"); + xmlWriter.WriteStartElement("head"); + + xmlWriter.WriteStartElement("meta"); + xmlWriter.WriteAttributeString("name", "Generator"); + xmlWriter.WriteAttributeString("content", "Zune -- 1.3.5728.0"); + xmlWriter.WriteEndElement(); + + xmlWriter.WriteElementString("author", ""); + xmlWriter.WriteElementString("title", playlist); + xmlWriter.WriteEndElement(); + xmlWriter.WriteStartElement("body"); + xmlWriter.WriteStartElement("seq"); + } + + public override void WriteTrack(string trackName, IITTrack track) + { + xmlWriter.WriteStartElement("media"); + xmlWriter.WriteAttributeString("src", trackName); + xmlWriter.WriteEndElement(); + } + + public override void Close() + { + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + xmlWriter.WriteEndElement(); + xmlWriter.Close(); + } + } +}