├── src ├── Clickwheel │ ├── clickwheel.128.png │ ├── Resources │ │ └── ArtworkDB-empty │ ├── Parsers │ │ ├── iTunesDB │ │ │ ├── MHOD │ │ │ │ ├── StringMHOD.cs │ │ │ │ ├── UnknownMHOD.cs │ │ │ │ ├── PlaylistPositionMHOD.cs │ │ │ │ ├── MHODFactory.cs │ │ │ │ ├── UnicodeMHOD.cs │ │ │ │ ├── BaseMHODElement.cs │ │ │ │ ├── ArtworkStringMHOD.cs │ │ │ │ └── MenuIndexMHOD.cs │ │ │ ├── AlbumListContainer.cs │ │ │ ├── UnknownListContainer.cs │ │ │ ├── PlaylistListContainer.cs │ │ │ ├── PlaylistListV2Container.cs │ │ │ ├── TrackListContainer.cs │ │ │ ├── DatabaseHash │ │ │ │ ├── DatabaseHasher.cs │ │ │ │ ├── Hash72.cs │ │ │ │ └── HashInfo.cs │ │ │ ├── ListContainerHeader.cs │ │ │ ├── IdGenerator.cs │ │ │ ├── PodcastListAdapter.cs │ │ │ ├── iTunesDBRoot.cs │ │ │ └── PlaylistItem.cs │ │ ├── iTunesSD │ │ │ ├── ITunesSD.cs │ │ │ ├── Header.cs │ │ │ └── Entry.cs │ │ ├── Artwork │ │ │ ├── ImageListContainer.cs │ │ │ ├── IThmbFileListContainer.cs │ │ │ ├── ImageAlbumListContainer.cs │ │ │ ├── UnknownListContainer.cs │ │ │ ├── ArtworkHelper.cs │ │ │ ├── MHODType2.cs │ │ │ ├── ImageAlbumItem.cs │ │ │ ├── IThmbFile.cs │ │ │ ├── ImageAlbumList.cs │ │ │ ├── IThmbFileList.cs │ │ │ ├── ListContainerHeader.cs │ │ │ ├── PhotoDB.cs │ │ │ ├── ArtworkDBRoot.cs │ │ │ ├── ImageList.cs │ │ │ └── ImageAlbum.cs │ │ ├── PlayCounts │ │ │ ├── Entry.cs │ │ │ ├── Header.cs │ │ │ └── PlayCounts.cs │ │ ├── Base │ │ │ ├── BaseDatabaseElement.cs │ │ │ └── BaseDatabase.cs │ │ ├── iTunesCDB │ │ │ └── ITunesCDBRoot.cs │ │ └── MusicDatabase.cs │ ├── Exceptions │ │ ├── UnsupportedArtworkFormatException.cs │ │ ├── IPodNotFoundException.cs │ │ ├── InvalidValueException.cs │ │ ├── InvalidIPodDriveException.cs │ │ ├── ExtendedSysInfoNotFoundException.cs │ │ ├── OperationNotAllowedException.cs │ │ ├── UnknownSortOrderException.cs │ │ ├── UnsupportedIPodException.cs │ │ ├── ArtworkDBNotFoundException.cs │ │ ├── NoSupportedArtworkException.cs │ │ ├── OutOfDiskSpaceException.cs │ │ ├── ParseException.cs │ │ ├── ITunesLockException.cs │ │ ├── BaseClickwheelException.cs │ │ ├── TrackAlreadyExistsException.cs │ │ └── UnsupportedITunesVersionException.cs │ ├── iPodFamilyEnum.cs │ ├── DataTypes │ │ ├── IPodTrackSize.cs │ │ ├── IPodTrackLength.cs │ │ ├── IPodDateTime.cs │ │ └── IPodRating.cs │ ├── Clickwheel.cs │ ├── IPodDevice │ │ └── FileSystems │ │ │ ├── IPodDriveInfo.cs │ │ │ └── IDeviceInfo.cs │ ├── Session.cs │ ├── Clickwheel.csproj │ ├── NewTrack.cs │ ├── packages.lock.json │ ├── IPodBackup.cs │ └── DebugLogger.cs ├── Clickwheel.DeviceHelper.GUI │ ├── icon.ico │ ├── icon.png │ ├── NativeMethods.txt │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── README.md │ ├── App.xaml │ ├── Clickwheel.DeviceHelper.GUI.csproj │ ├── MainWindow.xaml │ ├── packages.lock.json │ └── MainWindow.xaml.cs └── Clickwheel.DeviceHelper │ ├── NativeMethods.txt │ ├── DeviceHelper.cs │ ├── ScsiPassThroughWithBuffers.cs │ ├── Clickwheel.DeviceHelper.csproj │ └── packages.lock.json ├── tests └── Clickwheel.Tests │ ├── Fixtures │ ├── HashInfo │ ├── sample-image-red.png │ ├── ipod-test-db-hashed.db │ ├── sample-image-blue.png │ └── sample-image-green.png │ ├── TestConfig.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Parsers │ ├── iTunesDB │ │ └── DatabaseHash │ │ │ ├── Hash58Test.cs │ │ │ ├── HashInfoTest.cs │ │ │ └── Hash72Test.cs │ ├── Artwork │ │ └── ArtworkHelperTest.cs │ └── HelpersTest.cs │ └── Clickwheel.Tests.csproj ├── .gitignore ├── clickwheel-logo.svg ├── clickwheel-wordmark-on-dark.svg ├── clickwheel-wordmark-on-light.svg └── README.md /src/Clickwheel/clickwheel.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/src/Clickwheel/clickwheel.128.png -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/src/Clickwheel.DeviceHelper.GUI/icon.ico -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/src/Clickwheel.DeviceHelper.GUI/icon.png -------------------------------------------------------------------------------- /src/Clickwheel/Resources/ArtworkDB-empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/src/Clickwheel/Resources/ArtworkDB-empty -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Fixtures/HashInfo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/tests/Clickwheel.Tests/Fixtures/HashInfo -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | DwmSetWindowAttribute 2 | SHGetStockIconInfo 3 | DestroyIcon 4 | SHSTOCKICONINFO 5 | SHGSI_FLAGS -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Fixtures/sample-image-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/tests/Clickwheel.Tests/Fixtures/sample-image-red.png -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Fixtures/ipod-test-db-hashed.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/tests/Clickwheel.Tests/Fixtures/ipod-test-db-hashed.db -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Fixtures/sample-image-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/tests/Clickwheel.Tests/Fixtures/sample-image-blue.png -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Fixtures/sample-image-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dstaley/clickwheel/HEAD/tests/Clickwheel.Tests/Fixtures/sample-image-green.png -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/StringMHOD.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Parsers.iTunesDB 2 | { 3 | abstract class StringMHOD : BaseMHODElement 4 | { 5 | public abstract string Data { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper/NativeMethods.txt: -------------------------------------------------------------------------------- 1 | CreateFile 2 | DeviceIoControl 3 | SCSI_PASS_THROUGH 4 | _SCSI_PASS_THROUGH 5 | SCSI_PASS_THROUGH_WITH_BUFFERS 6 | STORAGE_DEVICE_NUMBER 7 | IOCTL_SCSI_PASS_THROUGH 8 | IOCTL_STORAGE_GET_DEVICE_NUMBER -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper/DeviceHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.DeviceHelper; 2 | 3 | public static class DeviceHelper 4 | { 5 | public static string GetExtendedSysInfoFromDrive(string driveLetter) 6 | { 7 | return DeviceXml.Get(driveLetter); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace Clickwheel.DeviceHelper.GUI 10 | { 11 | /// 12 | /// Interaction logic for App.xaml 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/UnsupportedArtworkFormatException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | public class UnsupportedArtworkFormatException : BaseClickwheelException 4 | { 5 | public UnsupportedArtworkFormatException(uint imageSize) 6 | : base($"The artwork format (size {imageSize}) is not currently supported.") 7 | { 8 | Category = "Unsupported Artwork format."; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/IPodNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when an iPod couldn't be found during a call to GetConnectedIPod() 5 | /// 6 | public class IPodNotFoundException : BaseClickwheelException 7 | { 8 | public IPodNotFoundException(string message) : base(message) 9 | { 10 | Category = "iPod could not be found"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/InvalidValueException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when an invalid value is specified for a track or playlist property 5 | /// 6 | public class InvalidValueException : BaseClickwheelException 7 | { 8 | public InvalidValueException(string message) : base(message) 9 | { 10 | Category = "Invalid Value Specified"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/InvalidIPodDriveException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when an invalid drive is specified to a call to IPod.GetIPodByDrive() 5 | /// 6 | public class InvalidIPodDriveException : BaseClickwheelException 7 | { 8 | public InvalidIPodDriveException(string message) : base(message) 9 | { 10 | Category = "iPod Not Found"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper/ScsiPassThroughWithBuffers.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using Windows.Win32.Storage.IscsiDisc; 3 | 4 | namespace Clickwheel.DeviceHelper 5 | { 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal unsafe struct ScsiPassThroughWithBuffers 8 | { 9 | public SCSI_PASS_THROUGH spt; 10 | public uint Filler; 11 | public fixed byte ucSenseBuf[32]; 12 | public fixed byte ucDataBuf[255]; 13 | } 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build results 2 | [Dd]ebug/ 3 | [Dd]ebugPublic/ 4 | [Rr]elease/ 5 | [Rr]eleases/ 6 | x64/ 7 | x86/ 8 | [Ww][Ii][Nn]32/ 9 | [Aa][Rr][Mm]/ 10 | [Aa][Rr][Mm]64/ 11 | bld/ 12 | [Bb]in/ 13 | [Oo]bj/ 14 | [Ll]og/ 15 | [Ll]ogs/ 16 | 17 | # Visual Studio 2015/2017 cache/options directory 18 | .vs/ 19 | 20 | # MSTest test Results 21 | [Tt]est[Rr]esult*/ 22 | [Bb]uild[Ll]og.* 23 | 24 | *.DotSettings.user 25 | 26 | .DS_Store 27 | 28 | # Snapshot test files 29 | *.received.* -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/ExtendedSysInfoNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown if ExtendedSysInfo was not found 5 | /// 6 | public class ExtendedSysInfoNotFoundException : BaseClickwheelException 7 | { 8 | public ExtendedSysInfoNotFoundException() : base("ExtendedSysInfo file not found on iPod.") 9 | { 10 | Category = "ExtendedSysInfo not found"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/OperationNotAllowedException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when (for example) trying to add/remove tracks from a Smart Playlist. 5 | /// 6 | public class OperationNotAllowedException : BaseClickwheelException 7 | { 8 | public OperationNotAllowedException(string message) : base(message) 9 | { 10 | Category = "Operation not allowed"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/UnknownSortOrderException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown if a Playlist's SortOrder field is not a value enumerated by Clickwheel. 5 | /// 6 | public class UnknownSortOrderException : BaseClickwheelException 7 | { 8 | public UnknownSortOrderException(string message) : base(message) 9 | { 10 | Category = "The playlist's Sort Order value is not supported"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/UnsupportedIPodException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when the iPod is not supported. This can be achieved by setting iPod.IsWritable=false. 5 | /// 6 | /// 7 | public class UnsupportedIPodException : BaseClickwheelException 8 | { 9 | public UnsupportedIPodException(string message) : base(message) 10 | { 11 | Category = "iPod not fully supported"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/ArtworkDBNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown if artwork is added to an iPod without an ArtworkDB 5 | /// 6 | public class ArtworkDBNotFoundException : BaseClickwheelException 7 | { 8 | public ArtworkDBNotFoundException() 9 | : base("iPod ArtworkDB not found. You cannot add or remove artwork.") 10 | { 11 | Category = "Artwork Problem"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/NoSupportedArtworkException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when trying to add album artwork and the iPod reported no supported artwork. 5 | /// 6 | public class NoSupportedArtworkException : BaseClickwheelException 7 | { 8 | public NoSupportedArtworkException() : base("No supported artwork formats were detected") 9 | { 10 | Category = "Album art could not be added"; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/TestConfig.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.CompilerServices; 3 | using VerifyTests; 4 | 5 | namespace Clickwheel.Tests 6 | { 7 | public static class TestConfig 8 | { 9 | [ModuleInitializer] 10 | public static void Init() 11 | { 12 | VerifierSettings.DerivePathInfo( 13 | (sourceFile, projectDirectory, type, method) => 14 | new(Path.Combine(projectDirectory, "Fixtures", "Snapshots"), type.Name, method.Name) 15 | ); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/OutOfDiskSpaceException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when adding tracks and adding new artwork to the iPod. Clickwheel will make sure there will be at least 10Mb of free space on the 5 | /// iPod after copying the track. 6 | /// 7 | public class OutOfDiskSpaceException : BaseClickwheelException 8 | { 9 | public OutOfDiskSpaceException(string message) : base(message) 10 | { 11 | Category = "The iPod cannot store any more files"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/README.md: -------------------------------------------------------------------------------- 1 | # Clickwheel Device Helper 2 | 3 | Clickwheel Device Helper is a Windows application that automatically writes the `SysInfoExtended` file to all connected iPods. 4 | 5 | ## Installation 6 | 7 | Clickwheel Device Helper requires the installation of the [.NET Desktop Runtime 6](https://dotnet.microsoft.com/en-us/download/dotnet/6.0). 8 | 9 | [Download Clickwheel Device Helper](https://github.com/dstaley/clickwheel/releases/tag/clickwheel-device-helper-v1.0.0) 10 | 11 | ## Usage 12 | 13 | Run `Clickwheel Device Helper.exe` with your iPod connected to your computer, and then click "Start". -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper/Clickwheel.DeviceHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-windows 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | all 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/ParseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Clickwheel.Exceptions 4 | { 5 | /// 6 | /// Thrown when the iPod database format could not be recognized or validated. 7 | /// This could occur if the iTunes file format changes or a 3rd party application has written the 8 | /// database in a different way to iTunes. 9 | /// 10 | public class ParseException : BaseClickwheelException 11 | { 12 | public ParseException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | Category = "Your iPod database could not be read"; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | [assembly: AssemblyTitle("Clickwheel.Tests")] 5 | [assembly: AssemblyDescription("")] 6 | [assembly: AssemblyConfiguration("")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("Clickwheel.Tests")] 9 | [assembly: AssemblyCopyright("Copyright © 2022")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | [assembly: ComVisible(false)] 14 | 15 | [assembly: Guid("6f8de93f-68e3-45cf-99ee-f8d7c4c72625")] 16 | 17 | // [assembly: AssemblyVersion("1.0.*")] 18 | [assembly: AssemblyVersion("1.0.0.0")] 19 | [assembly: AssemblyFileVersion("1.0.0.0")] 20 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/ITunesLockException.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Exceptions 2 | { 3 | /// 4 | /// Thrown when an iTunesLock file exists on the iPod. This usually means iTunes has locked the iPod 5 | /// and is currently syncing. 6 | /// 7 | public class ITunesLockException : BaseClickwheelException 8 | { 9 | public ITunesLockException(string lockFilePath) 10 | : base( 11 | $"iTunes has locked the iPod database. Please wait for iTunes to finish synchronizing. \r\nIf iTunes is not running, delete the '{lockFilePath}' file." 12 | ) 13 | { 14 | Category = "iPod cannot be opened"; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Clickwheel/iPodFamilyEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel 2 | { 3 | /// 4 | /// Enumeration of the different iPod types 5 | /// 6 | public enum IPodFamily 7 | { 8 | Unknown = 0, 9 | iPod_Gen1_Gen2 = 1, 10 | iPod_Gen3 = 2, 11 | iPod_Mini = 3, 12 | iPod_Gen4 = 4, 13 | iPod_Gen4_2 = 5, 14 | iPod_Gen5 = 6, 15 | iPod_Nano_Gen1 = 7, 16 | iPod_Nano_Gen2 = 9, 17 | iPod_Classic = 11, 18 | iPod_Nano_Gen3 = 12, 19 | iPod_Nano_Gen4 = 15, 20 | iPod_Nano_Gen5 = 16, 21 | iPod_Shuffle_Gen1 = 128, 22 | iPod_Shuffle_Gen2 = 130, 23 | iPod_Shuffle_Gen3 = 132, 24 | iPod_Shuffle_Gen4 = 133 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/BaseClickwheelException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Clickwheel.Exceptions 4 | { 5 | public class BaseClickwheelException : Exception 6 | { 7 | private string _category; 8 | 9 | public BaseClickwheelException(string message) : base(message) 10 | { 11 | _category = "Clickwheel Exception"; 12 | } 13 | 14 | public BaseClickwheelException(string message, Exception innerException) 15 | : base(message, innerException) 16 | { 17 | _category = "Clickwheel Exception"; 18 | } 19 | 20 | public string Category 21 | { 22 | get => _category; 23 | set => _category = value; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/UnknownMHOD.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | class UnknownMHOD : BaseMHODElement 6 | { 7 | private byte[] _byteData; 8 | 9 | internal override void Read(IPod iPod, BinaryReader reader) 10 | { 11 | _byteData = reader.ReadBytes(_sectionSize - _headerSize); 12 | } 13 | 14 | internal override void Write(BinaryWriter writer) 15 | { 16 | base.Write(writer); 17 | writer.Write(_byteData); 18 | } 19 | 20 | internal override int GetSectionSize() 21 | { 22 | var size = _headerSize + _byteData.Length; 23 | 24 | return size; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/TrackAlreadyExistsException.cs: -------------------------------------------------------------------------------- 1 | using Clickwheel.Parsers.iTunesDB; 2 | 3 | namespace Clickwheel.Exceptions 4 | { 5 | /// 6 | /// Thrown when trying to add a track to the iPod which already exists. (Same Title, Artist, Album, TrackNumber) 7 | /// 8 | public class TrackAlreadyExistsException : BaseClickwheelException 9 | { 10 | private Track _existingTrack; 11 | 12 | public TrackAlreadyExistsException(string message, Track existingTrack) : base(message) 13 | { 14 | Category = "This track already exists on your iPod"; 15 | _existingTrack = existingTrack; 16 | } 17 | 18 | /// 19 | /// Track that is on the iPod already 20 | /// 21 | public Track ExistingTrack => _existingTrack; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Clickwheel/Exceptions/UnsupportedITunesVersionException.cs: -------------------------------------------------------------------------------- 1 | using Clickwheel.Parsers; 2 | 3 | namespace Clickwheel.Exceptions 4 | { 5 | /// 6 | /// Thrown when the iPod database version is below 0x14. iTunes 7.1 and above create 0x14(+) databases. 7 | /// If the version is below 0x14, Clickwheel will try and read it, but will not enable modifications. 8 | /// 9 | public class UnsupportedITunesVersionException : BaseClickwheelException 10 | { 11 | private CompatibilityType _compatibility; 12 | 13 | public UnsupportedITunesVersionException(string message, CompatibilityType compatibility) 14 | : base(message) 15 | { 16 | Category = "iPod database not supported"; 17 | _compatibility = compatibility; 18 | } 19 | 20 | public CompatibilityType Compatibility => _compatibility; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Parsers/iTunesDB/DatabaseHash/Hash58Test.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using Clickwheel.DatabaseHash; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Clickwheel.Tests.Parsers.iTunesDB.DatabaseHash 7 | { 8 | [TestClass] 9 | public class Hash58Test 10 | { 11 | [TestMethod] 12 | [DeploymentItem(@"Fixtures/ipod-test-db-hashed.db")] 13 | public void TestHash58() 14 | { 15 | byte[] db = File.ReadAllBytes("ipod-test-db-hashed.db"); 16 | byte[] result = Hash58.GenerateDatabaseHash("000A27001A26973B", db); 17 | StringBuilder sb = new StringBuilder(); 18 | foreach (byte b in result) 19 | { 20 | sb.AppendFormat("{0:x}", b); 21 | } 22 | Assert.AreEqual("9134cb64dfa38c16dbcfb023768ddcaf8fbe34a2", sb.ToString()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Clickwheel/DataTypes/IPodTrackSize.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Clickwheel.Parsers; 3 | 4 | namespace Clickwheel.DataTypes 5 | { 6 | /// 7 | /// Wraps a file size in bytes and a human-readable string describing the size. 8 | /// 9 | public class IPodTrackSize : IComparable 10 | { 11 | uint _trackSize; 12 | string _trackSizeMB; 13 | 14 | public IPodTrackSize(uint trackSizeInBytes) 15 | { 16 | _trackSize = trackSizeInBytes; 17 | _trackSizeMB = Helpers.GetFileSizeString(trackSizeInBytes, 1); 18 | } 19 | 20 | public uint ByteCount => _trackSize; 21 | 22 | public override string ToString() 23 | { 24 | return _trackSizeMB; 25 | } 26 | 27 | #region IComparable Members 28 | 29 | public int CompareTo(object obj) 30 | { 31 | return _trackSize.CompareTo(((IPodTrackSize)obj).ByteCount); 32 | } 33 | 34 | #endregion 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesSD/ITunesSD.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesSD 4 | { 5 | class ITunesSD 6 | { 7 | IPod _iPod; 8 | Header _header; 9 | 10 | public ITunesSD(IPod iPod) 11 | { 12 | _iPod = iPod; 13 | _header = new Header(iPod); 14 | } 15 | 16 | public void Backup() 17 | { 18 | var iTunesSDPath = _iPod.FileSystem.ITunesSDPath; 19 | if (_iPod.FileSystem.FileExists(iTunesSDPath)) 20 | { 21 | File.Copy(iTunesSDPath, iTunesSDPath + ".spbackup", true); 22 | } 23 | } 24 | 25 | public void Generate() 26 | { 27 | var iTunesSDPath = _iPod.FileSystem.ITunesSDPath; 28 | var fs = new FileStream(iTunesSDPath, FileMode.Create, FileAccess.Write); 29 | var writer = new BinaryWriter(fs); 30 | 31 | _header.Write(writer); 32 | writer.Flush(); 33 | writer.Close(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/AlbumListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | /// 6 | /// Implements a type 4 (Album list) MHSD entry in iTunesDB 7 | /// 8 | class AlbumListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | private byte[] _unk1; 12 | 13 | public AlbumListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | var length = _header.SectionSize - _header.HeaderSize; 22 | _unk1 = reader.ReadBytes(length); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | writer.Write(_unk1); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.SectionSize; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | /// 6 | /// Implements a type 1 (Image list) MHSD entry in ArtworkDB 7 | /// 8 | class ImageListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | ImageList _childSection; 12 | 13 | public ImageListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | _childSection = new ImageList(); 22 | _childSection.Read(iPod, reader); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | _childSection.Write(writer); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.HeaderSize + _childSection.GetSectionSize(); 33 | } 34 | 35 | internal ImageList ImageList => _childSection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/IThmbFileListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | /// 6 | /// Implements a type 3 (file list) MHSD entry in ArtworkDB 7 | /// 8 | class IThmbFileListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | IThmbFileList _childSection; 12 | 13 | public IThmbFileListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | _childSection = new IThmbFileList(); 22 | _childSection.Read(iPod, reader); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | _childSection.Write(writer); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.HeaderSize + _childSection.GetSectionSize(); 33 | } 34 | 35 | internal IThmbFileList FileList => _childSection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageAlbumListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | /// 6 | /// Implements a type 2 (Image album list) MHSD entry in ArtworkDB / PhotoDB 7 | /// 8 | class ImageAlbumListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | ImageAlbumList _childSection; 12 | 13 | public ImageAlbumListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | _childSection = new ImageAlbumList(); 22 | _childSection.Read(iPod, reader); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | _childSection.Write(writer); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.HeaderSize + _childSection.GetSectionSize(); 33 | } 34 | 35 | internal ImageAlbumList ImageAlbumList => _childSection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/UnknownListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | /// 6 | /// Implements any unknown type MHSD entry in iTunesDB 7 | /// Simply reads to the end of the list, ignoring the contents. 8 | /// Role is to protect against future iTunesDB changes. 9 | /// 10 | class UnknownListContainer : BaseDatabaseElement 11 | { 12 | private ListContainerHeader _header; 13 | private byte[] _unk1; 14 | 15 | public UnknownListContainer(ListContainerHeader parent) 16 | { 17 | _header = parent; 18 | } 19 | 20 | internal override void Read(IPod iPod, BinaryReader reader) 21 | { 22 | base.Read(iPod, reader); 23 | var length = _header.SectionSize - _header.HeaderSize; 24 | _unk1 = reader.ReadBytes(length); 25 | } 26 | 27 | internal override void Write(BinaryWriter writer) 28 | { 29 | writer.Write(_unk1); 30 | } 31 | 32 | internal override int GetSectionSize() 33 | { 34 | return _header.SectionSize; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/UnknownListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | /// 6 | /// Implements any unknown type MHSD entry in iTunesDB 7 | /// Simply reads to the end of the list, ignoring the contents. 8 | /// Role is to protect against future iTunesDB changes. 9 | /// 10 | class UnknownListContainer : BaseDatabaseElement 11 | { 12 | private ListContainerHeader _header; 13 | private byte[] _unk1; 14 | 15 | public UnknownListContainer(ListContainerHeader parent) 16 | { 17 | _header = parent; 18 | } 19 | 20 | internal override void Read(IPod iPod, BinaryReader reader) 21 | { 22 | base.Read(iPod, reader); 23 | var length = _header.SectionSize - _header.HeaderSize; 24 | _unk1 = reader.ReadBytes(length); 25 | } 26 | 27 | internal override void Write(BinaryWriter writer) 28 | { 29 | writer.Write(_unk1); 30 | } 31 | 32 | internal override int GetSectionSize() 33 | { 34 | return _header.SectionSize; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Clickwheel/Clickwheel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.IPodDevice.FileSystems; 4 | 5 | namespace Clickwheel 6 | { 7 | /// 8 | /// Contains Clickwheel-specific methods. 9 | /// 10 | public static class Clickwheel 11 | { 12 | private static List _registeredFileSystems = new List(); 13 | 14 | static Clickwheel() 15 | { 16 | var iPodProfile = new StandardFileSystem( 17 | "IPod", 18 | Path.Combine("iPod_Control", "iTunes"), 19 | @"iPod_Control", 20 | Path.Combine("iPod_Control", "Artwork"), 21 | @"Photos" 22 | ); 23 | iPodProfile.ParseDbFilesLocally = true; 24 | _registeredFileSystems.Add(iPodProfile); 25 | } 26 | 27 | /// 28 | /// List of device FileSystems Clickwheel will use when searching for iPods. This list can be updated 29 | /// dynamically before calling GetConnectediPod(). 30 | /// 31 | public static List RegisteredFileSystems => _registeredFileSystems; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Clickwheel/DataTypes/IPodTrackLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Clickwheel.Parsers; 3 | 4 | namespace Clickwheel.DataTypes 5 | { 6 | /// 7 | /// Wraps a track length in milliseconds and a human-readable hh:mm:ss string 8 | /// 9 | public class IPodTrackLength : IComparable 10 | { 11 | uint _trackLengthMSecs; 12 | string _trackLengthMinsSecs; 13 | 14 | public IPodTrackLength(uint trackLengthInMSecs) 15 | { 16 | _trackLengthMSecs = trackLengthInMSecs; 17 | _trackLengthMinsSecs = Helpers.GetTimeString(this.Seconds); 18 | } 19 | 20 | public IPodTrackLength(int trackLengthInMSecs) : this((uint)trackLengthInMSecs) { } 21 | 22 | public uint Seconds => _trackLengthMSecs / 1000; 23 | 24 | public uint MilliSeconds => _trackLengthMSecs; 25 | 26 | public override string ToString() 27 | { 28 | return _trackLengthMinsSecs; 29 | } 30 | 31 | #region IComparable Members 32 | 33 | public int CompareTo(object obj) 34 | { 35 | return _trackLengthMSecs.CompareTo(((IPodTrackLength)obj).MilliSeconds); 36 | } 37 | 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/PlaylistListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | /// 6 | /// Implements a type 2 (Playlists list) MHSD entry in iTunesDB 7 | /// 8 | class PlaylistListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | private PlaylistList _childSection; 12 | 13 | internal PlaylistListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | _childSection = new PlaylistList(); 22 | _childSection.Read(iPod, reader); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | _childSection.Write(writer); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.HeaderSize + _childSection.GetSectionSize(); 33 | } 34 | 35 | internal PlaylistList GetPlaylistsList() 36 | { 37 | return _childSection; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/PlaylistListV2Container.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | /// 6 | /// Implements a type 3 (Playlist v2 list) MHSD entry in iTunesDB 7 | /// 8 | class PlaylistListV2Container : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | private PlaylistList _childSection; 12 | 13 | public PlaylistListV2Container(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | internal override void Read(IPod iPod, BinaryReader reader) 19 | { 20 | base.Read(iPod, reader); 21 | _childSection = new PlaylistList(); 22 | _childSection.Read(iPod, reader); 23 | } 24 | 25 | internal override void Write(BinaryWriter writer) 26 | { 27 | _childSection.Write(writer); 28 | } 29 | 30 | internal override int GetSectionSize() 31 | { 32 | return _header.HeaderSize + _childSection.GetSectionSize(); 33 | } 34 | 35 | internal PlaylistList GetPlaylistsList() 36 | { 37 | return _childSection; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Clickwheel/IPodDevice/FileSystems/IPodDriveInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.IPodDevice.FileSystems 4 | { 5 | public class IPodDriveInfo 6 | { 7 | private DriveInfo _driveInfo; 8 | private DirectoryInfo _directoryInfo; 9 | 10 | public IPodDriveInfo(DriveInfo driveInfo) 11 | { 12 | _driveInfo = driveInfo; 13 | } 14 | 15 | public IPodDriveInfo(DirectoryInfo directoryInfo) 16 | { 17 | _directoryInfo = directoryInfo; 18 | } 19 | 20 | public IPodDriveInfo(string drivePath) 21 | { 22 | _driveInfo = new DriveInfo(drivePath); 23 | } 24 | 25 | public bool IsReady => _driveInfo?.IsReady ?? true; 26 | 27 | public DriveType DriveType => _driveInfo?.DriveType ?? DriveType.Fixed; 28 | 29 | public string Name => _driveInfo?.Name ?? _directoryInfo.FullName; 30 | 31 | public long TotalSize => 32 | _driveInfo?.TotalSize ?? new DriveInfo(_directoryInfo.Root.FullName).TotalSize; 33 | 34 | public long AvailableFreeSpace => 35 | _driveInfo?.AvailableFreeSpace 36 | ?? new DriveInfo(_directoryInfo.Root.FullName).AvailableFreeSpace; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/TrackListContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | /// 6 | /// Implements a type 1 (Tracks list) MHSD entry in iTunesDB 7 | /// 8 | class TrackListContainer : BaseDatabaseElement 9 | { 10 | private ListContainerHeader _header; 11 | TrackList _childSection; 12 | 13 | public TrackListContainer(ListContainerHeader parent) 14 | { 15 | _header = parent; 16 | } 17 | 18 | #region IDatabaseElement Members 19 | 20 | internal override void Read(IPod iPod, BinaryReader reader) 21 | { 22 | base.Read(iPod, reader); 23 | _childSection = new TrackList(); 24 | _childSection.Read(iPod, reader); 25 | } 26 | 27 | internal override void Write(BinaryWriter writer) 28 | { 29 | _childSection.Write(writer); 30 | } 31 | 32 | internal override int GetSectionSize() 33 | { 34 | return _header.HeaderSize + _childSection.GetSectionSize(); 35 | } 36 | 37 | #endregion 38 | 39 | internal TrackList GetTrackList() 40 | { 41 | return _childSection; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Clickwheel/Session.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.Parsers.iTunesDB; 4 | 5 | namespace Clickwheel 6 | { 7 | class Session 8 | { 9 | IPod _iPod; 10 | public List DeletedTracks { get; set; } 11 | public List DeletedPlaylists { get; set; } 12 | 13 | public Session(IPod iPod) 14 | { 15 | _iPod = iPod; 16 | DeletedTracks = new List(); 17 | DeletedPlaylists = new List(); 18 | 19 | //clear out any per-session files for this iPod 20 | if (Directory.Exists(TempFilesPath)) 21 | { 22 | Directory.Delete(TempFilesPath, true); 23 | } 24 | 25 | Directory.CreateDirectory(TempFilesPath); 26 | } 27 | 28 | /// 29 | /// Folder used for storing per-session temporary files 30 | /// 31 | public string TempFilesPath => 32 | Path.Combine( 33 | Path.GetTempPath(), 34 | "Clickwheel", 35 | "Sessions", 36 | _iPod.DeviceInfo.SerialNumber 37 | ); 38 | 39 | public void Clear() 40 | { 41 | DeletedPlaylists.Clear(); 42 | DeletedTracks.Clear(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net6.0-windows7.0": { 5 | "Microsoft.Windows.CsWin32": { 6 | "type": "Direct", 7 | "requested": "[0.1.635-beta, )", 8 | "resolved": "0.1.635-beta", 9 | "contentHash": "brXs1R12DrxiC/iKvG9c86UzXflw/pdhY3HPJjZlbu8Md/lwN9X3H24N5ywCsszLhoTXQc0deJQcLilPzbNsDg==", 10 | "dependencies": { 11 | "Microsoft.Windows.SDK.Win32Docs": "0.1.7-alpha", 12 | "Microsoft.Windows.SDK.Win32Metadata": "17.0.2-preview", 13 | "System.Memory": "4.5.4" 14 | } 15 | }, 16 | "Microsoft.Windows.SDK.Win32Docs": { 17 | "type": "Transitive", 18 | "resolved": "0.1.7-alpha", 19 | "contentHash": "+8ygOfBME88Sq75zMopFfqCdxBxZoKDlizPK6iYxzSGf6j51rd9ISNJ20UKfOSr746tgn9GNyVI+jc0JXfnuZA==" 20 | }, 21 | "Microsoft.Windows.SDK.Win32Metadata": { 22 | "type": "Transitive", 23 | "resolved": "17.0.2-preview", 24 | "contentHash": "1sI2+VPcO0HK1/OI1n1iZQluv4vL+P8kfbkJJIN8fbjZbNJhJLwpKyixNyuele+hg63Q7LKAPwJpft/hIEgZSg==" 25 | }, 26 | "System.Memory": { 27 | "type": "Transitive", 28 | "resolved": "4.5.4", 29 | "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" 30 | } 31 | }, 32 | "net6.0-windows7.0/win-arm64": {} 33 | } 34 | } -------------------------------------------------------------------------------- /src/Clickwheel/DataTypes/IPodDateTime.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Clickwheel.Parsers; 3 | 4 | namespace Clickwheel.DataTypes 5 | { 6 | /// 7 | /// Wraps a .NET DateTime and an iPod-format timestamp. 8 | /// 9 | public class IPodDateTime : IComparable 10 | { 11 | uint _timeStamp; 12 | DateTime _dateTime; 13 | 14 | public IPodDateTime(uint timestamp) 15 | { 16 | _dateTime = Helpers.GetDateTimeFromTimeStamp(timestamp); 17 | _timeStamp = timestamp; 18 | } 19 | 20 | public IPodDateTime(DateTime date) 21 | { 22 | _dateTime = date; 23 | _timeStamp = Helpers.GetTimeStampFromDate(_dateTime); 24 | } 25 | 26 | public DateTime DateTime => _dateTime; 27 | 28 | public uint TimeStamp => _timeStamp; 29 | 30 | public override string ToString() 31 | { 32 | if (_timeStamp == 0) 33 | { 34 | return string.Empty; 35 | } 36 | else 37 | { 38 | return _dateTime.ToString(); 39 | } 40 | } 41 | 42 | #region IComparable Members 43 | 44 | public int CompareTo(object obj) 45 | { 46 | return _timeStamp.CompareTo(((IPodDateTime)obj).TimeStamp); 47 | } 48 | 49 | #endregion 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/Clickwheel.DeviceHelper.GUI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | Major 7 | enable 8 | true 9 | true 10 | true 11 | true 12 | x64;arm64 13 | icon.ico 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ArtworkHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using SixLabors.ImageSharp.Processing; 6 | 7 | namespace Clickwheel.Parsers.Artwork 8 | { 9 | class ArtworkHelper 10 | { 11 | public static byte[] GenerateResizedImageBytes( 12 | Image originalImage, 13 | SupportedArtworkFormat format 14 | ) where T : unmanaged, IPixel 15 | { 16 | using (var clone = originalImage.CloneAs()) 17 | { 18 | clone.Mutate(x => x.Resize((int)format.Width, (int)format.Height)); 19 | var pixelArray = new byte[clone.Width * clone.Height * Unsafe.SizeOf()]; 20 | clone.CopyPixelDataTo(pixelArray); 21 | return pixelArray; 22 | } 23 | } 24 | 25 | public static byte[] GenerateResizedImageBytes( 26 | Image originalImage, 27 | SupportedArtworkFormat format 28 | ) 29 | { 30 | switch (format.PixelFormat) 31 | { 32 | case PixelFormat.Rgb565: 33 | return GenerateResizedImageBytes(originalImage, format); 34 | default: 35 | throw new Exception($"Unsupported pixel format: {format.PixelFormat}"); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/PlayCounts/Entry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.PlayCounts 5 | { 6 | class Entry : BaseDatabaseElement 7 | { 8 | private int _playCount; 9 | private DateTime _lastPlayed; 10 | private int _bookmarkPosition; 11 | private int _rating; 12 | 13 | public Entry(int entrySize) 14 | { 15 | _headerSize = entrySize; 16 | _requiredHeaderSize = 16; 17 | } 18 | 19 | internal override void Read(IPod iPod, BinaryReader reader) 20 | { 21 | base.Read(iPod, reader); 22 | 23 | _playCount = reader.ReadInt32(); 24 | _lastPlayed = Helpers.GetDateTimeFromTimeStamp(reader.ReadUInt32()); 25 | _bookmarkPosition = reader.ReadInt32(); 26 | _rating = reader.ReadInt32(); 27 | 28 | ReadToHeaderEnd(reader); 29 | } 30 | 31 | internal override void Write(BinaryWriter writer) 32 | { 33 | throw new Exception("The method or operation is not implemented."); 34 | } 35 | 36 | internal override int GetSectionSize() 37 | { 38 | throw new Exception("The method or operation is not implemented."); 39 | } 40 | 41 | internal int PlayCount => _playCount; 42 | 43 | internal DateTime DateLastPlayed => _lastPlayed; 44 | 45 | internal int Rating => _rating / 20; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /clickwheel-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesSD/Header.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.iTunesSD 5 | { 6 | class Header : BaseDatabaseElement 7 | { 8 | private int _trackCount; 9 | private byte[] _unk1; 10 | private byte[] _headerPadding; 11 | 12 | public Header(IPod iPod) 13 | { 14 | _iPod = iPod; 15 | _unk1 = new byte[3]; 16 | _unk1[0] = 1; 17 | _unk1[1] = 6; 18 | _unk1[2] = 0; 19 | _headerSize = 18; 20 | _headerPadding = new byte[9]; 21 | 22 | _trackCount = _iPod.Tracks.Count; 23 | } 24 | 25 | internal override void Read(IPod iPod, BinaryReader reader) 26 | { 27 | throw new Exception("The method or operation is not implemented."); 28 | } 29 | 30 | internal override void Write(BinaryWriter writer) 31 | { 32 | writer.Write(Helpers.IntToITunesSDFormat(_trackCount)); 33 | writer.Write(_unk1); 34 | writer.Write(Helpers.IntToITunesSDFormat(_headerSize)); 35 | writer.Write(_headerPadding); 36 | 37 | foreach (var track in _iPod.Tracks) 38 | { 39 | var entry = new Entry(track); 40 | entry.Write(writer); 41 | } 42 | } 43 | 44 | internal override int GetSectionSize() 45 | { 46 | throw new Exception("The method or operation is not implemented."); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Clickwheel/DataTypes/IPodRating.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Clickwheel.DataTypes 4 | { 5 | /// 6 | /// Wraps an iPod-format rating and a human-readable Star Rating 7 | /// 8 | public class IPodRating : IComparable 9 | { 10 | byte _rating; 11 | string _ratingString; 12 | 13 | internal IPodRating(byte iTunesRating) 14 | { 15 | if (iTunesRating < 0 || iTunesRating > 100) 16 | { 17 | iTunesRating = 0; 18 | } 19 | _rating = iTunesRating; 20 | _ratingString = new string('*', (int)_rating / 20); 21 | } 22 | 23 | public IPodRating(int starRating) 24 | { 25 | if (starRating < 0) 26 | { 27 | starRating = 0; 28 | } 29 | else if (starRating > 5) 30 | { 31 | starRating = 5; 32 | } 33 | 34 | _rating = (byte)(starRating * 20); 35 | _ratingString = new string('*', starRating); 36 | } 37 | 38 | public int StarRating => (int)_rating / 20; 39 | 40 | internal byte ITunesRating => _rating; 41 | 42 | public override string ToString() 43 | { 44 | return _ratingString; 45 | } 46 | 47 | #region IComparable Members 48 | 49 | public int CompareTo(object obj) 50 | { 51 | return StarRating.CompareTo(((IPodRating)obj).StarRating); 52 | } 53 | 54 | #endregion 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Clickwheel/Clickwheel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Clickwheel 4 | https://github.com/dstaley/clickwheel.git 5 | git 6 | LGPL-3.0-or-later 7 | README.md 8 | iPod 9 | A modern cross-platform iPod management API for .NET 10 | clickwheel.128.png 11 | 0.1.1 12 | net7.0 13 | latest-All 14 | true 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <_Parameter1>Clickwheel.Tests 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/MHODType2.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Clickwheel.Parsers.iTunesDB; 3 | 4 | namespace Clickwheel.Parsers.Artwork 5 | { 6 | /// 7 | /// 8 | /// 9 | class MHODType2 : BaseMHODElement 10 | { 11 | private IPodImageFormat _childElement; 12 | 13 | internal MHODType2() 14 | { 15 | _requiredHeaderSize = 16; 16 | _headerSize = 24; 17 | _identifier = "mhod".ToCharArray(); 18 | _type = 2; 19 | _childElement = new IPodImageFormat(false); 20 | } 21 | 22 | #region IDatabaseElement Members 23 | 24 | internal override void Read(IPod iPod, BinaryReader reader) 25 | { 26 | _childElement.Read(iPod, reader); 27 | } 28 | 29 | internal override void Write(BinaryWriter writer) 30 | { 31 | base.Write(writer); 32 | _childElement.Write(writer); 33 | } 34 | 35 | internal override int GetSectionSize() 36 | { 37 | return _headerSize + _childElement.GetSectionSize(); 38 | } 39 | 40 | #endregion 41 | 42 | internal IPodImageFormat ArtworkFormat => _childElement; 43 | 44 | internal void Create(IPod iPod, SupportedArtworkFormat format, byte[] imageData) 45 | { 46 | _iPod = iPod; 47 | _unusedHeader = new byte[_headerSize - _requiredHeaderSize]; 48 | _childElement = new IPodImageFormat(false); 49 | _childElement.Create(_iPod, format, imageData); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Parsers/iTunesDB/DatabaseHash/HashInfoTest.cs: -------------------------------------------------------------------------------- 1 | using Clickwheel.DatabaseHash; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace Clickwheel.Tests.Parsers.iTunesDB.DatabaseHash 5 | { 6 | [TestClass] 7 | public class HashInfoTest 8 | { 9 | [TestMethod] 10 | public void TestGenerate() 11 | { 12 | var info = new HashInfo(); 13 | info.Generate("000A27001EE92D51"); 14 | CollectionAssert.AreEqual( 15 | new byte[] { 0x48, 0x41, 0x53, 0x48, 0x76, 0x30 }, 16 | info.Header); 17 | CollectionAssert.AreEqual( 18 | new byte[] { 0x00, 0x0a, 0x27, 0x00, 0x1e, 0xe9, 0x2d, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 19 | info.Uuid); 20 | Assert.AreEqual(12, info.RndPart.Length); 21 | Assert.AreEqual(16, info.Iv.Length); 22 | } 23 | 24 | [TestMethod] 25 | [DeploymentItem(@"Fixtures/HashInfo")] 26 | public void TestRead() 27 | { 28 | var info = new HashInfo(); 29 | info.Read("HashInfo"); 30 | CollectionAssert.AreEqual( 31 | new byte[] { 0x48, 0x41, 0x53, 0x48, 0x76, 0x30 }, 32 | info.Header); 33 | CollectionAssert.AreEqual( 34 | new byte[] { 0x00, 0x0a, 0x27, 0x00, 0x1e, 0xe9, 0x2d, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 35 | info.Uuid); 36 | Assert.AreEqual(12, info.RndPart.Length); 37 | Assert.AreEqual(16, info.Iv.Length); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/PlaylistPositionMHOD.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | class PlaylistPositionMHOD : BaseMHODElement 6 | { 7 | private byte[] _byteData; 8 | protected int _position; 9 | 10 | public int Position 11 | { 12 | get => _position; 13 | set => _position = value; 14 | } 15 | 16 | public PlaylistPositionMHOD() : base() 17 | { 18 | _type = MHODElementType.PlaylistPosition; 19 | } 20 | 21 | internal override void Read(IPod iPod, BinaryReader reader) 22 | { 23 | if (_sectionSize == _headerSize) { } 24 | else 25 | { 26 | _position = reader.ReadInt32(); 27 | _byteData = reader.ReadBytes(_sectionSize - (_headerSize + 4)); 28 | } 29 | } 30 | 31 | public void Create() 32 | { 33 | _byteData = new byte[16]; 34 | } 35 | 36 | internal override void Write(BinaryWriter writer) 37 | { 38 | if (writer.BaseStream.Position == 1192046) { } 39 | base.Write(writer); 40 | writer.Write(_position); 41 | if (_byteData != null) 42 | { 43 | writer.Write(_byteData); 44 | } 45 | } 46 | 47 | internal override int GetSectionSize() 48 | { 49 | var size = _headerSize + 4; 50 | if (_byteData != null) 51 | { 52 | size += _byteData.Length; 53 | } 54 | 55 | return size; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Clickwheel.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | Library 5 | false 6 | 9 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | PreserveNewest 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/PlayCounts/Header.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Clickwheel.Parsers.PlayCounts 6 | { 7 | class Header : BaseDatabaseElement 8 | { 9 | int _entrySize; 10 | int _nbrEntries; 11 | List _entries; 12 | 13 | public Header() 14 | { 15 | _requiredHeaderSize = 16; 16 | _entries = new List(); 17 | } 18 | 19 | internal override void Read(IPod iPod, BinaryReader reader) 20 | { 21 | base.Read(iPod, reader); 22 | _identifier = reader.ReadChars(4); 23 | _headerSize = reader.ReadInt32(); 24 | 25 | ValidateHeader("mhdp"); 26 | 27 | _entrySize = reader.ReadInt32(); 28 | _nbrEntries = reader.ReadInt32(); 29 | 30 | this.ReadToHeaderEnd(reader); 31 | 32 | for (var i = 0; i < _nbrEntries; i++) 33 | { 34 | var entry = new Entry(_entrySize); 35 | entry.Read(iPod, reader); 36 | _entries.Add(entry); 37 | } 38 | } 39 | 40 | internal override void Write(BinaryWriter writer) 41 | { 42 | throw new Exception("The method or operation is not implemented."); 43 | } 44 | 45 | internal override int GetSectionSize() 46 | { 47 | throw new Exception("The method or operation is not implemented."); 48 | } 49 | 50 | public IEnumerable Entries() 51 | { 52 | foreach (var e in _entries) 53 | { 54 | yield return e; 55 | } 56 | } 57 | 58 | public int EntryCount => _nbrEntries; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageAlbumItem.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.Parsers.iTunesDB; 4 | 5 | namespace Clickwheel.Parsers.Artwork 6 | { 7 | // Implements a MHIA entry in ArtworkDB 8 | /// 9 | /// An Image Album 10 | /// 11 | public class ImageAlbumItem : BaseDatabaseElement 12 | { 13 | int _unk1; 14 | 15 | private List _dataObjects = new List(); 16 | private List _images = new List(); 17 | internal uint ImageId { get; set; } 18 | internal IPodImage Artwork { get; set; } 19 | 20 | internal ImageAlbumItem() 21 | { 22 | _requiredHeaderSize = 20; 23 | } 24 | 25 | internal override void Read(IPod iPod, BinaryReader reader) 26 | { 27 | base.Read(iPod, reader); 28 | 29 | _identifier = reader.ReadChars(4); 30 | _headerSize = reader.ReadInt32(); 31 | 32 | ValidateHeader("mhia"); 33 | 34 | _sectionSize = reader.ReadInt32(); 35 | _unk1 = reader.ReadInt32(); 36 | ImageId = reader.ReadUInt32(); 37 | 38 | base.ReadToHeaderEnd(reader); 39 | } 40 | 41 | internal override void Write(BinaryWriter writer) 42 | { 43 | _sectionSize = GetSectionSize(); 44 | 45 | writer.Write(_identifier); 46 | writer.Write(_headerSize); 47 | writer.Write(_sectionSize); 48 | writer.Write(_unk1); 49 | writer.Write(ImageId); 50 | writer.Write(_unusedHeader); 51 | } 52 | 53 | internal override int GetSectionSize() 54 | { 55 | return _sectionSize; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/DatabaseHash/DatabaseHasher.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.DatabaseHash 4 | { 5 | internal static class DatabaseHasher 6 | { 7 | public static void Hash(FileStream file, IPod iPod) 8 | { 9 | if (iPod.DeviceInfo.FirewireId == null || iPod.DeviceInfo.FirewireId.Length != 16) 10 | { 11 | return; 12 | } 13 | 14 | byte[] hash = null; 15 | 16 | using var reader = new BinaryReader(file); 17 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 18 | var contents = new byte[reader.BaseStream.Length]; 19 | reader.Read(contents, 0, contents.Length); 20 | 21 | Zero(ref contents, 0x18, 8); 22 | Zero(ref contents, 0x32, 20); 23 | Zero(ref contents, 0x58, 20); 24 | 25 | hash = Hash58.GenerateDatabaseHash(iPod.DeviceInfo.FirewireId, contents); 26 | 27 | using var writer = new BinaryWriter(file); 28 | writer.Seek(0x58, SeekOrigin.Begin); 29 | writer.Write(hash, 0, hash.Length); 30 | 31 | if (iPod.ITunesDB.HashingScheme >= 2) 32 | { 33 | Zero(ref contents, 0x72, 46); 34 | 35 | var hashInfo = new HashInfo(); 36 | hashInfo.ReadOrGenerate(iPod.FileSystem.HashInfoPath, iPod.DeviceInfo.FirewireId); 37 | hash = Hash72.GenerateDatabaseHash(hashInfo, contents); 38 | 39 | writer.Seek(0x72, SeekOrigin.Begin); 40 | writer.Write(hash, 0, 46); 41 | } 42 | } 43 | 44 | private static void Zero(ref byte[] buffer, int index, int length) 45 | { 46 | for (var i = index; i < index + length; i++) 47 | { 48 | buffer[i] = 0; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/MHODFactory.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | class MHODFactory 6 | { 7 | public static BaseMHODElement ReadMHOD(IPod iPod, BinaryReader reader) 8 | { 9 | var mhod = new BaseMHODElement(); 10 | mhod.Read(iPod, reader); 11 | 12 | BaseMHODElement newMhod = null; 13 | 14 | switch (mhod.Type) 15 | { 16 | case MHODElementType.PodcastFileUrl: 17 | case MHODElementType.PodcastRSSUrl: 18 | newMhod = new UnknownMHOD(); 19 | break; 20 | 21 | case MHODElementType.Id: 22 | case MHODElementType.Title: 23 | case MHODElementType.FilePath: 24 | case MHODElementType.Album: 25 | case MHODElementType.Artist: 26 | case MHODElementType.Genre: 27 | case MHODElementType.FileType: 28 | case MHODElementType.Comment: 29 | case MHODElementType.Composer: 30 | case MHODElementType.AlbumArtist: 31 | case MHODElementType.DescriptionText: 32 | case MHODElementType.ArtistSortBy: 33 | newMhod = new UnicodeMHOD(); 34 | break; 35 | 36 | case MHODElementType.MenuIndexTable: 37 | newMhod = new MenuIndexMHOD(); 38 | break; 39 | 40 | case MHODElementType.PlaylistPosition: 41 | newMhod = new PlaylistPositionMHOD(); 42 | break; 43 | 44 | default: 45 | var umhod = new UnknownMHOD(); 46 | newMhod = new UnknownMHOD(); 47 | break; 48 | } 49 | newMhod.SetHeader(mhod); 50 | newMhod.Read(iPod, reader); 51 | return newMhod; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Clickwheel/IPodDevice/FileSystems/IDeviceInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Clickwheel.Parsers.Artwork; 4 | 5 | namespace Clickwheel.IPodDevice.FileSystems 6 | { 7 | /// 8 | /// Holds information about the iPod (Type, FirewireId, Serial #, supported artwork formats etc.) 9 | /// 10 | public interface IDeviceInfo 11 | { 12 | /// 13 | /// Exception which occured while retrieving device information 14 | /// 15 | Exception ReadException { get; } 16 | 17 | /// 18 | /// FirewireId of iPod. Used to generate iTunesDB database hash. 19 | /// 20 | string FirewireId { get; } 21 | 22 | /// 23 | /// Serial number of iPod. 24 | /// 25 | string SerialNumber { get; } 26 | 27 | /// 28 | /// Type of iPod. 29 | /// 30 | IPodFamily Family { get; } 31 | 32 | /// 33 | /// If Family is unknown, FamilyId can be used until Clickwheel is updated to include the new value in the IPodFamily enum. 34 | /// 35 | int FamilyId { get; } 36 | 37 | /// 38 | /// List of artwork formats supported by this iPod. 39 | /// 40 | List SupportedArtworkFormats { get; } 41 | 42 | /// 43 | /// List of photo formats supported by this iPod. 44 | /// 45 | List SupportedPhotoFormats { get; } 46 | 47 | /// 48 | /// Most disk-based iPod's can provide a device descriptor when queried. This is the raw result. Useful if you need more information about 49 | /// the iPod than IDeviceInfo provides. 50 | /// 51 | string RawDeviceDescriptor { get; } 52 | 53 | string OSVersion { get; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Clickwheel/NewTrack.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel 2 | { 3 | /// 4 | /// Used to add new tracks to the iPod 5 | /// 6 | public class NewTrack 7 | { 8 | /// 9 | /// Title of the track. Cannot be empty. 10 | /// 11 | public string Title; 12 | public string Artist; 13 | public string Album; 14 | public string Comments; 15 | 16 | /// 17 | /// Full path of the file to import. Can not be empty. 18 | /// 19 | public string FilePath; 20 | public string Genre; 21 | 22 | /// 23 | /// Length of track in milliseconds 24 | /// 25 | public uint Length; 26 | 27 | /// 28 | /// Bitrate in kb (e.g. 192) 29 | /// 30 | public uint Bitrate; 31 | public string Composer; 32 | 33 | /// 34 | /// Only displayed on the iPod for Podcast tracks 35 | /// 36 | public string DescriptionText; 37 | public uint TrackNumber; 38 | public uint Year; 39 | 40 | /// 41 | /// How many tracks in the album 42 | /// 43 | public uint AlbumTrackCount; 44 | 45 | /// 46 | /// Number of disc in the set 47 | /// 48 | public uint DiscNumber; 49 | 50 | /// 51 | /// How many discs in the set 52 | /// 53 | public uint TotalDiscCount; 54 | 55 | public string AlbumArtist; 56 | 57 | /// 58 | /// True if this item contains a video stream, otherwise false. Cannot be null. 59 | /// 60 | public bool? IsVideo; 61 | 62 | /// 63 | /// Path to an image file which will be used for the track's album art. Can be null. 64 | /// 65 | public string ArtworkFile; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/IThmbFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | // Implements a MHIF element in ArtworkDB 6 | /// 7 | /// 8 | /// 9 | class IThmbFile : BaseDatabaseElement 10 | { 11 | private uint _unk1, 12 | _formatId, 13 | _imageSize; 14 | 15 | public IThmbFile() 16 | { 17 | _requiredHeaderSize = 24; 18 | _headerSize = 124; 19 | _sectionSize = 124; 20 | } 21 | 22 | internal override void Read(IPod iPod, BinaryReader reader) 23 | { 24 | base.Read(iPod, reader); 25 | _identifier = reader.ReadChars(4); 26 | _headerSize = reader.ReadInt32(); 27 | 28 | ValidateHeader("mhif"); 29 | _sectionSize = reader.ReadInt32(); 30 | _unk1 = reader.ReadUInt32(); 31 | _formatId = reader.ReadUInt32(); 32 | _imageSize = reader.ReadUInt32(); 33 | 34 | ReadToHeaderEnd(reader); 35 | } 36 | 37 | internal override void Write(BinaryWriter writer) 38 | { 39 | _sectionSize = GetSectionSize(); 40 | 41 | writer.Write(_identifier); 42 | writer.Write(_headerSize); 43 | writer.Write(_sectionSize); 44 | writer.Write(_unk1); 45 | writer.Write(_formatId); 46 | writer.Write(_imageSize); 47 | writer.Write(_unusedHeader); 48 | } 49 | 50 | internal override int GetSectionSize() 51 | { 52 | return _sectionSize; 53 | } 54 | 55 | public uint FormatId => _formatId; 56 | 57 | public uint ImageSize => _imageSize; 58 | 59 | internal void Create(uint imageSize, uint formatId) 60 | { 61 | _identifier = "mhif".ToCharArray(); 62 | _unusedHeader = new byte[_headerSize - _requiredHeaderSize]; 63 | _imageSize = imageSize; 64 | _formatId = formatId; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Run as Administrator 33 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageAlbumList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.Artwork 5 | { 6 | // Implements a MHLA entry in ArtworkDB 7 | /// 8 | /// List of image albums 9 | /// 10 | public class ImageAlbumList : BaseDatabaseElement 11 | { 12 | private List _childSections; 13 | 14 | public ImageAlbumList() 15 | { 16 | _requiredHeaderSize = 12; 17 | _childSections = new List(); 18 | } 19 | 20 | internal override void Read(IPod iPod, BinaryReader reader) 21 | { 22 | base.Read(iPod, reader); 23 | _identifier = reader.ReadChars(4); 24 | _headerSize = reader.ReadInt32(); 25 | 26 | ValidateHeader("mhla"); 27 | 28 | var childCount = reader.ReadInt32(); 29 | 30 | this.ReadToHeaderEnd(reader); 31 | 32 | for (var i = 0; i < childCount; i++) 33 | { 34 | var album = new ImageAlbum(); 35 | album.Read(iPod, reader); 36 | _childSections.Add(album); 37 | } 38 | } 39 | 40 | internal override void Write(BinaryWriter writer) 41 | { 42 | _sectionSize = GetSectionSize(); 43 | 44 | writer.Write(_identifier); 45 | writer.Write(_headerSize); 46 | writer.Write(_childSections.Count); 47 | writer.Write(_unusedHeader); 48 | 49 | for (var i = 0; i < _childSections.Count; i++) 50 | { 51 | _childSections[i].Write(writer); 52 | } 53 | } 54 | 55 | internal override int GetSectionSize() 56 | { 57 | var size = _headerSize; 58 | for (var i = 0; i < _childSections.Count; i++) 59 | { 60 | size += _childSections[i].GetSectionSize(); 61 | } 62 | return size; 63 | } 64 | 65 | public List Albums => _childSections; 66 | 67 | internal void ResolveImages(ImageList images) 68 | { 69 | foreach (var album in _childSections) 70 | { 71 | album.ResolveImages(images); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/PlayCounts/PlayCounts.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using Clickwheel.DataTypes; 4 | 5 | namespace Clickwheel.Parsers.PlayCounts 6 | { 7 | class PlayCounts 8 | { 9 | Header _header; 10 | MusicDatabase _iTunesDB; 11 | 12 | public PlayCounts(MusicDatabase iTunesDB) 13 | { 14 | _iTunesDB = iTunesDB; 15 | var playCountsPath = _iTunesDB.iPod.FileSystem.PlayCountsPath; 16 | if (!_iTunesDB.iPod.FileSystem.FileExists(playCountsPath)) 17 | { 18 | return; 19 | } 20 | 21 | var fs = _iTunesDB.iPod.FileSystem.OpenFile(playCountsPath, FileAccess.Read); 22 | var br = new BinaryReader(fs); 23 | if (br.BaseStream.Length < 16) 24 | { 25 | br.Close(); 26 | return; 27 | } 28 | 29 | _header = new Header(); 30 | _header.Read(_iTunesDB.iPod, br); 31 | br.Close(); 32 | } 33 | 34 | public void MergeChanges() 35 | { 36 | //If we didnt read the file, cant merge any changes. 37 | if (_header == null) 38 | { 39 | return; 40 | } 41 | 42 | if (_header.EntryCount != _iTunesDB.TracksList.Count) 43 | { 44 | return; 45 | } 46 | 47 | var currentIndex = 0; 48 | 49 | foreach (var entry in _header.Entries()) 50 | { 51 | var track = _iTunesDB.TracksList[currentIndex]; 52 | if (entry.PlayCount > 0) 53 | { 54 | Debug.WriteLine("Updated playcount for " + track.Artist + " " + track.Title); 55 | track.PlayCount += entry.PlayCount; 56 | track.DateLastPlayed = new IPodDateTime(entry.DateLastPlayed); 57 | } 58 | if (track.Rating.StarRating != entry.Rating) 59 | { 60 | track.Rating = new IPodRating(entry.Rating); 61 | } 62 | 63 | currentIndex++; 64 | } 65 | 66 | var playCountsPath = _iTunesDB.iPod.FileSystem.PlayCountsPath; 67 | _iTunesDB.iPod.FileSystem.DeleteFile(playCountsPath); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/DatabaseHash/Hash72.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.Security.Cryptography; 5 | 6 | namespace Clickwheel.DatabaseHash 7 | { 8 | [SuppressMessage( 9 | "Microsoft.Cryptography", 10 | "CA5350:Do Not Use Weak Cryptographic Algorithms", 11 | Justification = "iPods require SHA1" 12 | )] 13 | [SuppressMessage( 14 | "Security", 15 | "CA5401:Do not use CreateEncryptor with non-default IV", 16 | Justification = "iPods depend on using the same IV" 17 | )] 18 | internal static class Hash72 19 | { 20 | private static readonly byte[] AES_KEY = { 0x61, 0x8c, 0xa1, 0x0d, 0xc7, 0xf5, 0x7f, 0xd3, 0xb4, 0x72, 0x3e, 0x08, 0x15, 0x74, 0x63, 0xd7 }; 21 | 22 | public static byte[] GenerateDatabaseHash(HashInfo info, byte[] iTunesDB) 23 | { 24 | using var sha1 = SHA1.Create(); 25 | var sha1Digest = sha1.ComputeHash(iTunesDB); 26 | 27 | var hash = CalculateHash(sha1Digest, info.RndPart, info.Iv); 28 | return hash; 29 | } 30 | 31 | public static byte[] CalculateHash(byte[] digest, byte[] rndPart, byte[] iv) 32 | { 33 | var signature = new byte[46]; 34 | var plaintext = new byte[32]; 35 | 36 | Array.Copy(digest, plaintext, 20); 37 | Array.Copy(rndPart, 0, plaintext, 20, 12); 38 | 39 | signature[0] = 0x01; 40 | signature[1] = 0x00; 41 | 42 | Array.Copy(rndPart, 0, signature, 2, 12); 43 | 44 | var output = EncryptWithAes(plaintext, AES_KEY, iv); 45 | 46 | Array.Copy(output, 0, signature, 14, 32); 47 | 48 | return signature; 49 | } 50 | 51 | private static byte[] EncryptWithAes(byte[] plaintext, byte[] key, byte[] iv) 52 | { 53 | using var aes = Aes.Create(); 54 | aes.Key = key; 55 | aes.IV = iv; 56 | 57 | var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); 58 | using var ms = new MemoryStream(); 59 | using var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write); 60 | using (var bw = new BinaryWriter(cs)) 61 | { 62 | bw.Write(plaintext); 63 | } 64 | 65 | return ms.ToArray(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Parsers/iTunesDB/DatabaseHash/Hash72Test.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Clickwheel.DatabaseHash; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Clickwheel.Tests.Parsers.iTunesDB.DatabaseHash 6 | { 7 | [TestClass] 8 | public class Hash72Test 9 | { 10 | [TestMethod] 11 | [DeploymentItem(@"Fixtures/HashInfo")] 12 | [DeploymentItem(@"Fixtures/ipod-test-db-hashed.db")] 13 | public void TestRead() 14 | { 15 | var info = new HashInfo(); 16 | info.Read("HashInfo"); 17 | CollectionAssert.AreEqual( 18 | new byte[] 19 | { 20 | 0x00, 0x0a, 0x27, 0x00, 0x1e, 0xe9, 0x2d, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00 22 | }, 23 | info.Uuid); 24 | 25 | var db = File.ReadAllBytes("ipod-test-db-hashed.db"); 26 | var checksum = Hash72.GenerateDatabaseHash(info, db); 27 | CollectionAssert.AreEqual( 28 | new byte[] 29 | { 30 | 0x01, 0x00, 0x53, 0x34, 0xA0, 0xA1, 0x1D, 0x64, 0xF1, 0xE7, 0x5C, 0xE5, 0xB6, 0x8C, 0xE1, 0x52, 31 | 0x06, 0x18, 0xD6, 0x47, 0xAC, 0x8F, 0x56, 0x2B, 0x02, 0x3C, 0x9A, 0x7C, 0xB5, 0x29, 0x1C, 0x9E, 32 | 0x06, 0x92, 0x13, 0x0B, 0x24, 0xF0, 0x7E, 0x7F, 0xBD, 0x09, 0x1F, 0x03, 0xD8, 0x36 33 | }, 34 | checksum 35 | ); 36 | } 37 | 38 | [TestMethod] 39 | public void TestReadOrGenerate() 40 | { 41 | var info = new HashInfo(); 42 | var hashInfoPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 43 | info.ReadOrGenerate(hashInfoPath, "000A27001EE92D51"); 44 | CollectionAssert.AreEqual( 45 | new byte[] 46 | { 47 | 0x00, 0x0a, 0x27, 0x00, 0x1e, 0xe9, 0x2d, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00 49 | }, 50 | info.Uuid); 51 | 52 | var verifyInfo = new HashInfo(); 53 | verifyInfo.Read(hashInfoPath); 54 | CollectionAssert.AreEqual( 55 | new byte[] 56 | { 57 | 0x00, 0x0a, 0x27, 0x00, 0x1e, 0xe9, 0x2d, 0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00 59 | }, 60 | info.Uuid); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Parsers/Artwork/ArtworkHelperTest.cs: -------------------------------------------------------------------------------- 1 | using Clickwheel.Parsers.Artwork; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using SixLabors.ImageSharp; 4 | 5 | namespace Clickwheel.Tests.Parsers.Artwork 6 | { 7 | [TestClass] 8 | public class ArtworkHelperTest 9 | { 10 | [TestMethod] 11 | [DeploymentItem(@"Fixtures/sample-image-red.png")] 12 | [DeploymentItem(@"Fixtures/sample-image-green.png")] 13 | [DeploymentItem(@"Fixtures/sample-image-blue.png")] 14 | public void TestGenerateResizedImage() 15 | { 16 | using (var image = Image.Load("sample-image-red.png")) 17 | { 18 | var expected = new byte[] 19 | { 20 | 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 21 | 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 0x00, 0xf8, 22 | }; 23 | var format = new SupportedArtworkFormat(9999, PixelFormat.Rgb565); 24 | var actual = ArtworkHelper.GenerateResizedImageBytes(image, format); 25 | CollectionAssert.AreEqual(expected, actual); 26 | } 27 | 28 | using (var image = Image.Load("sample-image-green.png")) 29 | { 30 | var expected = new byte[] 31 | { 32 | 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 33 | 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07, 0xe0, 0x07 34 | }; 35 | 36 | var format = new SupportedArtworkFormat(9999, PixelFormat.Rgb565); 37 | var actual = ArtworkHelper.GenerateResizedImageBytes(image, format); 38 | CollectionAssert.AreEqual(expected, actual); 39 | } 40 | 41 | using (var image = Image.Load("sample-image-blue.png")) 42 | { 43 | var expected = new byte[] 44 | { 45 | 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 46 | 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00 47 | }; 48 | var format = new SupportedArtworkFormat(9999, PixelFormat.Rgb565); 49 | var actual = ArtworkHelper.GenerateResizedImageBytes(image, format); 50 | CollectionAssert.AreEqual(expected, actual); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/IThmbFileList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.Artwork 5 | { 6 | // Implements a MHLF entry in ArtworkDB 7 | /// 8 | /// List of ithmb artwork files 9 | /// 10 | class IThmbFileList : BaseDatabaseElement 11 | { 12 | private List _childSections; 13 | 14 | public IThmbFileList() 15 | { 16 | _requiredHeaderSize = 12; 17 | _childSections = new List(); 18 | } 19 | 20 | internal override void Read(IPod iPod, BinaryReader reader) 21 | { 22 | base.Read(iPod, reader); 23 | _identifier = reader.ReadChars(4); 24 | _headerSize = reader.ReadInt32(); 25 | 26 | ValidateHeader("mhlf"); 27 | 28 | var fileCount = reader.ReadInt32(); 29 | 30 | this.ReadToHeaderEnd(reader); 31 | 32 | for (var i = 0; i < fileCount; i++) 33 | { 34 | var file = new IThmbFile(); 35 | file.Read(iPod, reader); 36 | _childSections.Add(file); 37 | } 38 | } 39 | 40 | internal override void Write(BinaryWriter writer) 41 | { 42 | _sectionSize = GetSectionSize(); 43 | 44 | writer.Write(_identifier); 45 | writer.Write(_headerSize); 46 | writer.Write(_childSections.Count); 47 | writer.Write(_unusedHeader); 48 | 49 | for (var i = 0; i < _childSections.Count; i++) 50 | { 51 | _childSections[i].Write(writer); 52 | } 53 | } 54 | 55 | internal override int GetSectionSize() 56 | { 57 | var size = _headerSize; 58 | for (var i = 0; i < _childSections.Count; i++) 59 | { 60 | size += _childSections[i].GetSectionSize(); 61 | } 62 | return size; 63 | } 64 | 65 | /// 66 | /// Enumerates each IThmbFile in this list. 67 | /// 68 | /// 69 | public IEnumerable Files() 70 | { 71 | foreach (var file in _childSections) 72 | { 73 | yield return file; 74 | } 75 | } 76 | 77 | internal void AddIThmbFile(IPodImageFormat format) 78 | { 79 | var newFile = new IThmbFile(); 80 | newFile.Create(format.ImageSize, format.FormatId); 81 | _childSections.Add(newFile); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ListContainerHeader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.Artwork 4 | { 5 | internal enum MHSDSectionType 6 | { 7 | Images = 1, 8 | Albums = 2, 9 | Files = 3 10 | } 11 | 12 | /// 13 | /// Implements a generic MHSD entry in ArtworkDB 14 | /// 15 | class ListContainerHeader : BaseDatabaseElement 16 | { 17 | protected MHSDSectionType _type; 18 | protected BaseDatabaseElement _childSection; 19 | 20 | public ListContainerHeader() 21 | { 22 | _requiredHeaderSize = 16; 23 | } 24 | 25 | internal int HeaderSize => _headerSize; 26 | 27 | internal int SectionSize => _sectionSize; 28 | 29 | internal MHSDSectionType Type => _type; 30 | 31 | internal override void Read(IPod iPod, BinaryReader reader) 32 | { 33 | base.Read(iPod, reader); 34 | _identifier = reader.ReadChars(4); 35 | _headerSize = reader.ReadInt32(); 36 | 37 | ValidateHeader("mhsd"); 38 | 39 | _sectionSize = reader.ReadInt32(); 40 | _type = (MHSDSectionType)reader.ReadInt32(); 41 | ReadToHeaderEnd(reader); 42 | 43 | switch (_type) 44 | { 45 | case MHSDSectionType.Images: 46 | _childSection = new ImageListContainer(this); 47 | break; 48 | case MHSDSectionType.Files: 49 | _childSection = new IThmbFileListContainer(this); 50 | break; 51 | case MHSDSectionType.Albums: 52 | _childSection = new ImageAlbumListContainer(this); 53 | break; 54 | default: 55 | _childSection = new UnknownListContainer(this); 56 | break; 57 | } 58 | _childSection.Read(iPod, reader); 59 | } 60 | 61 | internal override void Write(BinaryWriter writer) 62 | { 63 | _sectionSize = GetSectionSize(); 64 | 65 | writer.Write(_identifier); 66 | writer.Write(_headerSize); 67 | writer.Write(_sectionSize); 68 | writer.Write((int)_type); 69 | writer.Write(_unusedHeader); 70 | 71 | _childSection.Write(writer); 72 | } 73 | 74 | internal override int GetSectionSize() 75 | { 76 | return _childSection.GetSectionSize(); 77 | } 78 | 79 | public BaseDatabaseElement GetListContainer() 80 | { 81 | return _childSection; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/PhotoDB.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Clickwheel.Exceptions; 4 | 5 | namespace Clickwheel.Parsers.Artwork 6 | { 7 | internal class PhotoDB : BaseDatabase 8 | { 9 | private ArtworkDBRoot _databaseRoot; 10 | private ImageAlbumList _albumList; 11 | private bool _isDirty = false; 12 | 13 | public PhotoDB(IPod iPod) 14 | { 15 | _iPod = iPod; 16 | _databaseFilePath = iPod.FileSystem.PhotoDBPath; 17 | } 18 | 19 | public override void Parse() 20 | { 21 | if (!_iPod.FileSystem.FileExists(_databaseFilePath)) 22 | { 23 | return; 24 | } 25 | 26 | _databaseRoot = new ArtworkDBRoot(); 27 | try 28 | { 29 | ReadDatabase(_databaseRoot); 30 | var imageList = ( 31 | (ImageListContainer)_databaseRoot 32 | .GetChildSection(MHSDSectionType.Images) 33 | .GetListContainer() 34 | ).ImageList; 35 | _albumList = ( 36 | (ImageAlbumListContainer)_databaseRoot 37 | .GetChildSection(MHSDSectionType.Albums) 38 | .GetListContainer() 39 | ).ImageAlbumList; 40 | 41 | foreach (var art in imageList.Images()) 42 | { 43 | art.IsPhotoFormat = true; 44 | } 45 | 46 | _albumList.ResolveImages(imageList); 47 | } 48 | catch (Exception ex) 49 | { 50 | //Swallow any PhotoDB parsing issues for now. We never write this file out anyway. 51 | DebugLogger.LogException(ex); 52 | } 53 | Trace.WriteLine("PhotoDB: " + _compatibility); 54 | } 55 | 56 | public override void Save() 57 | { 58 | if (_databaseRoot == null) 59 | { 60 | return; 61 | } 62 | 63 | AssertIsWritable(); 64 | Debug.WriteLine("Saving PhotoDB " + DateTime.Now); 65 | WriteDatabase(_databaseRoot); 66 | _isDirty = false; 67 | } 68 | 69 | public override bool IsDirty => _isDirty; 70 | 71 | public override void AssertIsWritable() 72 | { 73 | if (_databaseRoot == null) 74 | { 75 | throw new ArtworkDBNotFoundException(); 76 | } 77 | base.AssertIsWritable(); 78 | } 79 | 80 | public ImageAlbumList PhotoAlbumList => _albumList; 81 | 82 | public override int Version => 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/UnicodeMHOD.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Clickwheel.Parsers.iTunesDB 6 | { 7 | class UnicodeMHOD : StringMHOD 8 | { 9 | private int _dataSize; 10 | private int _unk3; 11 | private int _unk4; 12 | private string _data; 13 | private byte[] _unk5; 14 | protected int _position; 15 | 16 | public override string Data 17 | { 18 | get => _data; 19 | set => _data = value; 20 | } 21 | 22 | public int Position 23 | { 24 | get => _position; 25 | set => _position = value; 26 | } 27 | 28 | public UnicodeMHOD() : base() 29 | { 30 | _position = 1; 31 | _dataSize = 0; 32 | _data = string.Empty; 33 | _unk3 = 1; 34 | } 35 | 36 | public UnicodeMHOD(int type) : this() 37 | { 38 | _type = type; 39 | } 40 | 41 | internal override void Read(IPod iPod, BinaryReader reader) 42 | { 43 | _position = reader.ReadInt32(); 44 | _dataSize = reader.ReadInt32(); 45 | _unk3 = reader.ReadInt32(); 46 | _unk4 = reader.ReadInt32(); 47 | var byteData = reader.ReadBytes(_dataSize); 48 | _data = Encoding.Unicode.GetString(byteData); 49 | 50 | var extraDataLength = _sectionSize - (_dataSize + _headerSize + 16); 51 | if (extraDataLength > 0) 52 | { 53 | _unk5 = reader.ReadBytes(extraDataLength); 54 | } 55 | } 56 | 57 | internal override void Write(BinaryWriter writer) 58 | { 59 | base.Write(writer); 60 | 61 | var dataBytes = Array.Empty(); 62 | if (_data != null) 63 | { 64 | dataBytes = Encoding.Unicode.GetBytes(_data); 65 | } 66 | 67 | writer.Write(_position); 68 | writer.Write(dataBytes.Length); 69 | writer.Write(_unk3); 70 | writer.Write(_unk4); 71 | 72 | writer.Write(dataBytes); 73 | if (_unk5 != null) 74 | { 75 | writer.Write(_unk5); 76 | } 77 | } 78 | 79 | internal override int GetSectionSize() 80 | { 81 | var size = _headerSize; 82 | if (_data != null) 83 | { 84 | size += Encoding.Unicode.GetByteCount(_data); 85 | } 86 | size += 16; 87 | 88 | if (_unk5 != null) 89 | { 90 | size += _unk5.GetLength(0); 91 | } 92 | if (size != _sectionSize) { } 93 | 94 | return size; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Base/BaseDatabaseElement.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.Exceptions; 4 | using Clickwheel.Parsers.iTunesDB; 5 | 6 | namespace Clickwheel.Parsers 7 | { 8 | /// 9 | /// Internally used by Clickwheel. Should not be used from external code. 10 | /// 11 | public abstract class BaseDatabaseElement 12 | { 13 | protected char[] _identifier; 14 | protected int _headerSize; 15 | protected int _sectionSize; 16 | protected byte[] _unusedHeader; 17 | protected int _requiredHeaderSize; 18 | protected IPod _iPod; 19 | 20 | protected bool ValidateHeader(string validIdentifier) 21 | { 22 | var strIdentifier = new string(_identifier); 23 | if (strIdentifier != validIdentifier) 24 | { 25 | throw new ParseException( 26 | validIdentifier + " expected, but " + strIdentifier + " found", 27 | null 28 | ); 29 | } 30 | 31 | if (_headerSize < _requiredHeaderSize) 32 | { 33 | throw new UnsupportedITunesVersionException( 34 | $"Expected {strIdentifier} section with length {_requiredHeaderSize}, but found length {_headerSize}.", 35 | CompatibilityType.NotWritable 36 | ); 37 | } 38 | return true; 39 | } 40 | 41 | protected void ReadToHeaderEnd(BinaryReader reader) 42 | { 43 | var unusedHeaderSize = _headerSize - _requiredHeaderSize; 44 | _unusedHeader = new byte[unusedHeaderSize]; 45 | _unusedHeader = reader.ReadBytes(unusedHeaderSize); 46 | } 47 | 48 | internal virtual void Read(IPod iPod, BinaryReader reader) 49 | { 50 | _iPod = iPod; 51 | } 52 | 53 | internal abstract void Write(BinaryWriter writer); 54 | internal abstract int GetSectionSize(); 55 | 56 | internal StringMHOD GetChildByType(List children, int type) 57 | { 58 | for (var i = 0; i < children.Count; i++) 59 | { 60 | if (children[i] is StringMHOD && children[i].Type == type) 61 | { 62 | return (StringMHOD)children[i]; 63 | } 64 | } 65 | return null; 66 | } 67 | 68 | internal string GetDataElement(List children, int type) 69 | { 70 | var mhod = GetChildByType(children, type); 71 | if (mhod != null) 72 | { 73 | return mhod.Data; 74 | } 75 | else 76 | { 77 | return string.Empty; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/DatabaseHash/HashInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net.Http; 5 | 6 | namespace Clickwheel.DatabaseHash 7 | { 8 | internal class HashInfo 9 | { 10 | public byte[] Header; // 6 11 | public byte[] Uuid; // 20 12 | public byte[] RndPart; // 12 13 | public byte[] Iv; // 16 14 | 15 | public void Read(string hashInfoPath) 16 | { 17 | if (!File.Exists(hashInfoPath)) 18 | { 19 | throw new Exception($"Unable to find valid HashInfo at path {hashInfoPath}"); 20 | } 21 | 22 | using var fs = new FileStream( 23 | hashInfoPath, 24 | FileMode.OpenOrCreate, 25 | FileAccess.ReadWrite 26 | ); 27 | using var reader = new BinaryReader(fs); 28 | Header = reader.ReadBytes(6); 29 | Uuid = reader.ReadBytes(20); 30 | RndPart = reader.ReadBytes(12); 31 | Iv = reader.ReadBytes(16); 32 | } 33 | 34 | public void ReadOrGenerate(string hashInfoPath, string firewireId) 35 | { 36 | if (!File.Exists(hashInfoPath)) 37 | { 38 | Generate(firewireId); 39 | Write(hashInfoPath); 40 | return; 41 | } 42 | 43 | Read(hashInfoPath); 44 | } 45 | 46 | public void Generate(string firewireId) 47 | { 48 | if (firewireId.Length != 16) 49 | { 50 | throw new Exception("firewireId must be 16 characters long"); 51 | } 52 | 53 | using var client = new HttpClient(); 54 | using var request = new HttpRequestMessage(HttpMethod.Post, "https://ihash.marcan.st/") 55 | { 56 | Content = new FormUrlEncodedContent( 57 | new Dictionary { { "uuid", firewireId }, { "go", "Generate" } } 58 | ) 59 | }; 60 | var response = client.Send(request); 61 | 62 | using var reader = new BinaryReader(response.Content.ReadAsStream()); 63 | Header = reader.ReadBytes(6); 64 | Uuid = reader.ReadBytes(20); 65 | RndPart = reader.ReadBytes(12); 66 | Iv = reader.ReadBytes(16); 67 | } 68 | 69 | private void Write(string hashInfoPath) 70 | { 71 | using var fs = new FileStream( 72 | hashInfoPath, 73 | FileMode.OpenOrCreate, 74 | FileAccess.ReadWrite 75 | ); 76 | using var writer = new BinaryWriter(fs); 77 | writer.Write(Header); 78 | writer.Write(Uuid); 79 | writer.Write(RndPart); 80 | writer.Write(Iv); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesSD/Entry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Clickwheel.Parsers.iTunesDB; 5 | 6 | namespace Clickwheel.Parsers.iTunesSD 7 | { 8 | class Entry : BaseDatabaseElement 9 | { 10 | int _entrySize; 11 | int _unk1; 12 | byte[] _unk2; 13 | int _volume; 14 | int _unk3; 15 | string _fileName; 16 | bool _shuffleFlag; 17 | bool _bookmarkFlag; 18 | byte _unk4; 19 | 20 | public Entry(Track track) 21 | { 22 | _entrySize = 558; 23 | _unk1 = 0x5aa501; 24 | _unk2 = new byte[18]; 25 | _unk3 = 0x200; 26 | _volume = 0x64; 27 | _fileName = "/" + track.FilePath; 28 | _shuffleFlag = true; 29 | _bookmarkFlag = false; 30 | } 31 | 32 | internal override void Read(IPod iPod, BinaryReader reader) 33 | { 34 | throw new Exception("The method or operation is not implemented."); 35 | } 36 | 37 | internal override void Write(BinaryWriter writer) 38 | { 39 | writer.Write(Helpers.IntToITunesSDFormat(_entrySize)); 40 | writer.Write(Helpers.IntToITunesSDFormat(_unk1)); 41 | writer.Write(_unk2); 42 | writer.Write(Helpers.IntToITunesSDFormat(_volume)); 43 | writer.Write(Helpers.IntToITunesSDFormat(GetFileType())); 44 | writer.Write(Helpers.IntToITunesSDFormat(_unk3)); 45 | writer.Write(GetSDFormatFileName()); 46 | writer.Write(_shuffleFlag); 47 | writer.Write(_bookmarkFlag); 48 | writer.Write(_unk4); 49 | } 50 | 51 | internal override int GetSectionSize() 52 | { 53 | throw new Exception("The method or operation is not implemented."); 54 | } 55 | 56 | private byte[] GetSDFormatFileName() 57 | { 58 | _fileName = _fileName.Replace("\\", "/"); 59 | var bytes = new byte[522]; 60 | var filename = UnicodeEncoding.Unicode.GetBytes(_fileName); 61 | filename.CopyTo(bytes, 0); 62 | return bytes; 63 | } 64 | 65 | private int GetFileType() 66 | { 67 | string extension = null; 68 | if (_fileName.Length > 3) 69 | { 70 | extension = _fileName.ToLower().Substring(_fileName.Length - 3); 71 | } 72 | 73 | if (extension == "mp3") 74 | { 75 | return 1; 76 | } 77 | else if (extension == "aac" || extension == "m4a") 78 | { 79 | return 2; 80 | } 81 | else if (extension == "wav") 82 | { 83 | return 4; 84 | } 85 | else 86 | { 87 | return 0; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/ListContainerHeader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Clickwheel.Parsers.iTunesDB 4 | { 5 | internal enum MHSDSectionType 6 | { 7 | Tracks = 1, 8 | Playlists = 2, 9 | PlaylistsV2 = 3, 10 | Albums = 4, 11 | Type5, 12 | Type6, 13 | Unknown = 999 14 | } 15 | 16 | /// 17 | /// Implements a generic MHSD entry in iTunesDB 18 | /// 19 | class ListContainerHeader : BaseDatabaseElement 20 | { 21 | protected MHSDSectionType _type; 22 | protected BaseDatabaseElement _childElement; 23 | 24 | public ListContainerHeader() 25 | { 26 | _requiredHeaderSize = 16; 27 | } 28 | 29 | internal int HeaderSize => _headerSize; 30 | 31 | internal int SectionSize => _sectionSize; 32 | 33 | internal MHSDSectionType Type => _type; 34 | 35 | internal override void Read(IPod iPod, BinaryReader reader) 36 | { 37 | base.Read(iPod, reader); 38 | _identifier = reader.ReadChars(4); 39 | _headerSize = reader.ReadInt32(); 40 | 41 | ValidateHeader("mhsd"); 42 | 43 | _sectionSize = reader.ReadInt32(); 44 | _type = (MHSDSectionType)reader.ReadInt32(); 45 | ReadToHeaderEnd(reader); 46 | 47 | switch (_type) 48 | { 49 | case MHSDSectionType.Albums: 50 | _childElement = new AlbumListContainer(this); 51 | break; 52 | case MHSDSectionType.Tracks: 53 | _childElement = new TrackListContainer(this); 54 | break; 55 | case MHSDSectionType.Playlists: 56 | _childElement = new PlaylistListContainer(this); 57 | break; 58 | case MHSDSectionType.PlaylistsV2: 59 | _childElement = new PlaylistListV2Container(this); 60 | break; 61 | default: 62 | _childElement = new UnknownListContainer(this); 63 | break; 64 | } 65 | _childElement.Read(iPod, reader); 66 | } 67 | 68 | internal override void Write(BinaryWriter writer) 69 | { 70 | _sectionSize = GetSectionSize(); 71 | 72 | writer.Write(_identifier); 73 | writer.Write(_headerSize); 74 | writer.Write(_sectionSize); 75 | writer.Write((int)_type); 76 | writer.Write(_unusedHeader); 77 | 78 | _childElement.Write(writer); 79 | } 80 | 81 | internal override int GetSectionSize() 82 | { 83 | return _childElement.GetSectionSize(); 84 | } 85 | 86 | public BaseDatabaseElement GetListContainer() 87 | { 88 | return _childElement; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ArtworkDBRoot.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.Artwork 5 | { 6 | public class ArtworkDBRoot : BaseDatabaseElement 7 | { 8 | private int _unk1, 9 | _unk2, 10 | _listContainerCount, 11 | _unk3; 12 | private uint _nextImageId; 13 | 14 | private List _childSections; 15 | 16 | public ArtworkDBRoot() 17 | { 18 | _requiredHeaderSize = 32; 19 | _childSections = new List(); 20 | } 21 | 22 | internal override void Read(IPod iPod, BinaryReader reader) 23 | { 24 | base.Read(iPod, reader); 25 | _identifier = reader.ReadChars(4); 26 | _headerSize = reader.ReadInt32(); 27 | 28 | ValidateHeader("mhfd"); 29 | 30 | _sectionSize = reader.ReadInt32(); 31 | _unk1 = reader.ReadInt32(); 32 | _unk2 = reader.ReadInt32(); 33 | _listContainerCount = reader.ReadInt32(); 34 | _unk3 = reader.ReadInt32(); 35 | _nextImageId = reader.ReadUInt32(); 36 | 37 | ReadToHeaderEnd(reader); 38 | 39 | for (var i = 0; i < _listContainerCount; i++) 40 | { 41 | var containerHeader = new ListContainerHeader(); 42 | containerHeader.Read(iPod, reader); 43 | _childSections.Add(containerHeader); 44 | } 45 | } 46 | 47 | internal override void Write(BinaryWriter writer) 48 | { 49 | _sectionSize = GetSectionSize(); 50 | 51 | writer.Write(_identifier); 52 | writer.Write(_headerSize); 53 | writer.Write(_sectionSize); 54 | writer.Write(_unk1); 55 | writer.Write(_unk2); 56 | writer.Write(_childSections.Count); 57 | writer.Write(_unk3); 58 | writer.Write(_nextImageId); 59 | writer.Write(_unusedHeader); 60 | 61 | for (var i = 0; i < _childSections.Count; i++) 62 | { 63 | _childSections[i].Write(writer); 64 | } 65 | } 66 | 67 | internal override int GetSectionSize() 68 | { 69 | var size = _headerSize; 70 | for (var i = 0; i < _childSections.Count; i++) 71 | { 72 | size += _childSections[i].GetSectionSize(); 73 | } 74 | return size; 75 | } 76 | 77 | internal ListContainerHeader GetChildSection(MHSDSectionType type) 78 | { 79 | for (var i = 0; i < _childSections.Count; i++) 80 | { 81 | if (_childSections[i].Type == type) 82 | { 83 | return _childSections[i]; 84 | } 85 | } 86 | return null; 87 | } 88 | 89 | public uint NextImageId 90 | { 91 | get => _nextImageId; 92 | set => _nextImageId = value; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net6.0-windows7.0": { 5 | "Microsoft.Windows.CsWin32": { 6 | "type": "Direct", 7 | "requested": "[0.2.164-beta, )", 8 | "resolved": "0.2.164-beta", 9 | "contentHash": "FxKqWqtYHAeLvJPAH8q2R6AERm91N8eX051DQ+muY6zKlymqO2c5cHNEYeAdFJwNP0Hy4MYsYhqTWJdxFCgogg==", 10 | "dependencies": { 11 | "Microsoft.Windows.SDK.Win32Docs": "0.1.12-alpha", 12 | "Microsoft.Windows.SDK.Win32Metadata": "39.0.18-preview", 13 | "System.Memory": "4.5.5", 14 | "System.Runtime.CompilerServices.Unsafe": "6.0.0" 15 | } 16 | }, 17 | "WPF-UI": { 18 | "type": "Direct", 19 | "requested": "[2.0.3, )", 20 | "resolved": "2.0.3", 21 | "contentHash": "1e9Q8pQGfi9YFp3WavKZjXyLYohIjXxk5q0NqEaamofVgIdxRxwqrkWha584FBUISRBBT/qFOrl97YBl5bs30Q==", 22 | "dependencies": { 23 | "System.Drawing.Common": "6.0.0" 24 | } 25 | }, 26 | "Microsoft.Win32.SystemEvents": { 27 | "type": "Transitive", 28 | "resolved": "6.0.0", 29 | "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" 30 | }, 31 | "Microsoft.Windows.SDK.Win32Docs": { 32 | "type": "Transitive", 33 | "resolved": "0.1.12-alpha", 34 | "contentHash": "DcKRcvQkTP+aT1Y+GcrzhmZJ9zEXpwqrcQZNER0k+ksLUDl0hsAFMqhpI4aqwEEXG1qbLyAINqcZHE47kpqt6Q==" 35 | }, 36 | "Microsoft.Windows.SDK.Win32Metadata": { 37 | "type": "Transitive", 38 | "resolved": "39.0.18-preview", 39 | "contentHash": "h6ayEOK8HxGr4+dwqRno/grl7JZwPfqv7SyMmJs9aQbAJm+cGygAsKkN5iSkEok0sFWZcjW3xVmjyMKlVYkglg==" 40 | }, 41 | "System.Drawing.Common": { 42 | "type": "Transitive", 43 | "resolved": "6.0.0", 44 | "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", 45 | "dependencies": { 46 | "Microsoft.Win32.SystemEvents": "6.0.0" 47 | } 48 | }, 49 | "System.Memory": { 50 | "type": "Transitive", 51 | "resolved": "4.5.5", 52 | "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" 53 | }, 54 | "System.Runtime.CompilerServices.Unsafe": { 55 | "type": "Transitive", 56 | "resolved": "6.0.0", 57 | "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" 58 | }, 59 | "clickwheel.devicehelper": { 60 | "type": "Project" 61 | } 62 | }, 63 | "net6.0-windows7.0/win-arm64": { 64 | "Microsoft.Win32.SystemEvents": { 65 | "type": "Transitive", 66 | "resolved": "6.0.0", 67 | "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" 68 | }, 69 | "System.Drawing.Common": { 70 | "type": "Transitive", 71 | "resolved": "6.0.0", 72 | "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", 73 | "dependencies": { 74 | "Microsoft.Win32.SystemEvents": "6.0.0" 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /tests/Clickwheel.Tests/Parsers/HelpersTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Clickwheel.Parsers; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Clickwheel.Tests.Parsers 7 | { 8 | [TestClass] 9 | public class HelpersTest 10 | { 11 | private void AssertEqualDates(DateTime expected, DateTime actual) 12 | { 13 | Assert.IsTrue(expected.Equals(actual), $"expected {expected}, but received {actual}"); 14 | } 15 | 16 | [TestMethod] 17 | public void TestGetDateTimeFromTimeStamp() 18 | { 19 | AssertEqualDates(new DateTime(1904, 1, 1), Helpers.GetDateTimeFromTimeStamp(0)); 20 | AssertEqualDates(new DateTime(1970, 1, 1), Helpers.GetDateTimeFromTimeStamp(2082844800)); 21 | AssertEqualDates(new DateTime(2022, 1, 1), Helpers.GetDateTimeFromTimeStamp(3723840000)); 22 | } 23 | 24 | [TestMethod] 25 | public void TestGetTimeStampFromDate() 26 | { 27 | Assert.AreEqual((uint)0, Helpers.GetTimeStampFromDate(new DateTime(1904, 1, 1))); 28 | Assert.AreEqual((uint)2082844800, Helpers.GetTimeStampFromDate(new DateTime(1970, 1, 1))); 29 | Assert.AreEqual((uint)3723840000, Helpers.GetTimeStampFromDate(new DateTime(2022, 1, 1))); 30 | } 31 | 32 | [TestMethod] 33 | public void TestiPodPathToStandardPath() 34 | { 35 | Assert.AreEqual( 36 | Path.Combine("iPod_Control", "iTunes", "iTunesDB"), 37 | Helpers.iPodPathToStandardPath("iPod_Control:iTunes:iTunesDB") 38 | ); 39 | } 40 | 41 | [TestMethod] 42 | public void TestStandardPathToiPodPaths() 43 | { 44 | Assert.AreEqual("iPod_Control:iTunes:iTunesDB", Helpers.StandardPathToiPodPath(@"iPod_Control\iTunes\iTunesDB")); 45 | Assert.AreEqual("iPod_Control:iTunes:iTunesDB", Helpers.StandardPathToiPodPath(@"iPod_Control/iTunes/iTunesDB")); 46 | } 47 | 48 | [TestMethod] 49 | public void TestIntToITunesSDFormat() 50 | { 51 | CollectionAssert.AreEqual(new byte[] { 0, 0, 0 }, Helpers.IntToITunesSDFormat(0)); 52 | CollectionAssert.AreEqual(new byte[] { 0, 0, 255 }, Helpers.IntToITunesSDFormat(255)); 53 | CollectionAssert.AreEqual(new byte[] { 255, 255, 254 }, Helpers.IntToITunesSDFormat(16777214)); 54 | CollectionAssert.AreEqual(new byte[] { 255, 255, 255 }, Helpers.IntToITunesSDFormat(16777215)); 55 | } 56 | 57 | [TestMethod] 58 | public void GetTimeString() 59 | { 60 | Assert.AreEqual("00:00:00", Helpers.GetTimeString(0)); 61 | Assert.AreEqual("00:01:00", Helpers.GetTimeString(60)); 62 | Assert.AreEqual("01:00:00", Helpers.GetTimeString(3600)); 63 | Assert.AreEqual("99:59:59", Helpers.GetTimeString(359999)); 64 | } 65 | 66 | [TestMethod] 67 | public void TestGetFileSizeString() 68 | { 69 | Assert.AreEqual("0MB", Helpers.GetFileSizeString(0, 0)); 70 | Assert.AreEqual("1MB", Helpers.GetFileSizeString(1048576, 0)); 71 | Assert.AreEqual("1MB", Helpers.GetFileSizeString(1048577, 0)); 72 | Assert.AreEqual("1024MB", Helpers.GetFileSizeString(1073741824, 0)); 73 | Assert.AreEqual("2GB", Helpers.GetFileSizeString(2147483648, 0)); 74 | Assert.AreEqual("2.5GB", Helpers.GetFileSizeString(2684354560, 1)); 75 | Assert.AreEqual("1.4GB", Helpers.GetFileSizeString(1476395008, 3)); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/Clickwheel/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net7.0": { 5 | "Microsoft.Data.Sqlite": { 6 | "type": "Direct", 7 | "requested": "[6.0.4, )", 8 | "resolved": "6.0.4", 9 | "contentHash": "9jhBB4WYRXMIB9AwIRz4omdxNqZSFFj8ESilpH2hF9x+btVXzbrNCk/yK7Km4UQTr2QUoRlZrjFBmhecIwgCCQ==", 10 | "dependencies": { 11 | "Microsoft.Data.Sqlite.Core": "6.0.4", 12 | "SQLitePCLRaw.bundle_e_sqlite3": "2.0.6" 13 | } 14 | }, 15 | "SixLabors.ImageSharp": { 16 | "type": "Direct", 17 | "requested": "[2.1.0, )", 18 | "resolved": "2.1.0", 19 | "contentHash": "H8npUDq3VRagzRsJVxaNoUSMmD+kjAs7sR1Ip85eWbN5c0O1medMw/FiQ32dSNfWz/gVrc+xHs3es9SFlAiNBw==", 20 | "dependencies": { 21 | "System.Runtime.CompilerServices.Unsafe": "5.0.0", 22 | "System.Text.Encoding.CodePages": "5.0.0" 23 | } 24 | }, 25 | "Microsoft.Data.Sqlite.Core": { 26 | "type": "Transitive", 27 | "resolved": "6.0.4", 28 | "contentHash": "3TZX7R2aX1TX5m4A5Kj+SY633NJDeHDP6JiDRCwUnJGKC3IrHgnO8p+oT2hRZpN168qx4Ixe4T9C+xZdZc26gw==", 29 | "dependencies": { 30 | "SQLitePCLRaw.core": "2.0.6" 31 | } 32 | }, 33 | "Microsoft.NETCore.Platforms": { 34 | "type": "Transitive", 35 | "resolved": "5.0.0", 36 | "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" 37 | }, 38 | "SQLitePCLRaw.bundle_e_sqlite3": { 39 | "type": "Transitive", 40 | "resolved": "2.0.6", 41 | "contentHash": "zssYqiaucyGArZfg74rJuzK0ewgZiidsRVrZTmP7JLNvK806gXg6PGA46XzoJGpNPPA5uRcumwvVp6YTYxtQ5w==", 42 | "dependencies": { 43 | "SQLitePCLRaw.core": "2.0.6", 44 | "SQLitePCLRaw.lib.e_sqlite3": "2.0.6", 45 | "SQLitePCLRaw.provider.e_sqlite3": "2.0.6" 46 | } 47 | }, 48 | "SQLitePCLRaw.core": { 49 | "type": "Transitive", 50 | "resolved": "2.0.6", 51 | "contentHash": "Vh8n0dTvwXkCGur2WqQTITvk4BUO8i8h9ucSx3wwuaej3s2S6ZC0R7vqCTf9TfS/I4QkXO6g3W2YQIRFkOcijA==", 52 | "dependencies": { 53 | "System.Memory": "4.5.3" 54 | } 55 | }, 56 | "SQLitePCLRaw.lib.e_sqlite3": { 57 | "type": "Transitive", 58 | "resolved": "2.0.6", 59 | "contentHash": "xlstskMKalKQl0H2uLNe0viBM6fvAGLWqKZUQ3twX5y1tSOZKe0+EbXopQKYdbjJytNGI6y5WSKjpI+kVr2Ckg==" 60 | }, 61 | "SQLitePCLRaw.provider.e_sqlite3": { 62 | "type": "Transitive", 63 | "resolved": "2.0.6", 64 | "contentHash": "peXLJbhU+0clVBIPirihM1NoTBqw8ouBpcUsVMlcZ4k6fcL2hwgkctVB2Nt5VsbnOJcPspQL5xQK7QvLpxkMgg==", 65 | "dependencies": { 66 | "SQLitePCLRaw.core": "2.0.6" 67 | } 68 | }, 69 | "System.Memory": { 70 | "type": "Transitive", 71 | "resolved": "4.5.3", 72 | "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" 73 | }, 74 | "System.Runtime.CompilerServices.Unsafe": { 75 | "type": "Transitive", 76 | "resolved": "5.0.0", 77 | "contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==" 78 | }, 79 | "System.Text.Encoding.CodePages": { 80 | "type": "Transitive", 81 | "resolved": "5.0.0", 82 | "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", 83 | "dependencies": { 84 | "Microsoft.NETCore.Platforms": "5.0.0" 85 | } 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/BaseMHODElement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.iTunesDB 5 | { 6 | internal static class MHODElementType 7 | { 8 | public const int Id = 0; 9 | public const int Title = 1; 10 | public const int FilePath = 2; 11 | public const int Album = 3; 12 | public const int Artist = 4; 13 | public const int Genre = 5; 14 | public const int FileType = 6; 15 | public const int Comment = 8; 16 | public const int Composer = 12; 17 | public const int DescriptionText = 14; 18 | public const int PodcastFileUrl = 15; 19 | public const int PodcastRSSUrl = 16; 20 | public const int ChapterData = 17; 21 | public const int AlbumArtist = 22; 22 | public const int ArtistSortBy = 23; 23 | public const int TitleSortBy = 27; 24 | public const int AlbumSortBy = 28; 25 | public const int AlbumArtistSortBy = 29; 26 | public const int SmartPlaylistData = 50; 27 | public const int SmartPlaylistRule = 51; 28 | public const int MenuIndexTable = 52; 29 | public const int LetterJumpTable = 53; 30 | public const int PlaylistPosition = 100; 31 | } 32 | 33 | class BaseMHODElement : BaseDatabaseElement 34 | { 35 | protected int _type; 36 | protected int _unk1; 37 | protected int _unk2; 38 | 39 | internal int HeaderSize => _headerSize; 40 | internal int SectionSize => _sectionSize; 41 | 42 | internal int Unk1 => _unk1; 43 | internal int Unk2 => _unk2; 44 | 45 | public int Type 46 | { 47 | get => _type; 48 | set => _type = value; 49 | } 50 | 51 | internal BaseMHODElement() 52 | { 53 | _requiredHeaderSize = 24; 54 | 55 | _headerSize = 24; 56 | _identifier = "mhod".ToCharArray(); 57 | 58 | _type = MHODElementType.Id; 59 | } 60 | 61 | internal BaseMHODElement(int type) : this() 62 | { 63 | _type = type; 64 | } 65 | 66 | #region IDatabaseElement Members 67 | 68 | internal override void Read(IPod iPod, BinaryReader reader) 69 | { 70 | base.Read(iPod, reader); 71 | _identifier = reader.ReadChars(4); 72 | _headerSize = reader.ReadInt32(); 73 | 74 | ValidateHeader("mhod"); 75 | 76 | _sectionSize = reader.ReadInt32(); 77 | _type = reader.ReadInt32(); 78 | _unk1 = reader.ReadInt32(); 79 | _unk2 = reader.ReadInt32(); 80 | } 81 | 82 | internal override void Write(BinaryWriter writer) 83 | { 84 | _sectionSize = GetSectionSize(); 85 | 86 | writer.Write(_identifier); 87 | writer.Write(_headerSize); 88 | writer.Write(_sectionSize); 89 | writer.Write((int)_type); 90 | writer.Write(_unk1); 91 | writer.Write(_unk2); 92 | } 93 | 94 | internal override int GetSectionSize() 95 | { 96 | throw new NotImplementedException(); 97 | } 98 | 99 | #endregion 100 | 101 | internal void SetHeader(BaseMHODElement header) 102 | { 103 | _headerSize = header.HeaderSize; 104 | _sectionSize = header.SectionSize; 105 | _unk1 = header.Unk1; 106 | _unk2 = header.Unk2; 107 | _type = header.Type; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.Exceptions; 4 | using Clickwheel.Parsers.iTunesDB; 5 | using SixLabors.ImageSharp; 6 | 7 | namespace Clickwheel.Parsers.Artwork 8 | { 9 | // Implements a MHLI entry in ArtworkDB / PhotoDB 10 | /// 11 | /// List of iPod images 12 | /// 13 | class ImageList : BaseDatabaseElement 14 | { 15 | private List _childSections; 16 | 17 | public ImageList() 18 | { 19 | _requiredHeaderSize = 12; 20 | _childSections = new List(); 21 | } 22 | 23 | internal override void Read(IPod iPod, BinaryReader reader) 24 | { 25 | base.Read(iPod, reader); 26 | _identifier = reader.ReadChars(4); 27 | _headerSize = reader.ReadInt32(); 28 | 29 | ValidateHeader("mhli"); 30 | 31 | var imageCount = reader.ReadInt32(); 32 | 33 | this.ReadToHeaderEnd(reader); 34 | 35 | for (var i = 0; i < imageCount; i++) 36 | { 37 | var mhii = new IPodImage(); 38 | mhii.Read(iPod, reader); 39 | _childSections.Add(mhii); 40 | } 41 | } 42 | 43 | internal override void Write(BinaryWriter writer) 44 | { 45 | _sectionSize = GetSectionSize(); 46 | 47 | writer.Write(_identifier); 48 | writer.Write(_headerSize); 49 | writer.Write(_childSections.Count); 50 | writer.Write(_unusedHeader); 51 | 52 | for (var i = 0; i < _childSections.Count; i++) 53 | { 54 | _childSections[i].Write(writer); 55 | } 56 | } 57 | 58 | internal override int GetSectionSize() 59 | { 60 | var size = _headerSize; 61 | for (var i = 0; i < _childSections.Count; i++) 62 | { 63 | size += _childSections[i].GetSectionSize(); 64 | } 65 | return size; 66 | } 67 | 68 | internal IPodImage GetArtByTrackId(long dbId) 69 | { 70 | return _childSections.Find(a => a.TrackDBId == dbId && a.UsedCount > 0); 71 | } 72 | 73 | internal IPodImage GetArtById(uint id) 74 | { 75 | return _childSections.Find(a => a.Id == id); 76 | } 77 | 78 | internal void AddNewArtwork(Track track, Image image) 79 | { 80 | if (_iPod.DeviceInfo.SupportedArtworkFormats.Count == 0) 81 | { 82 | throw new NoSupportedArtworkException(); 83 | } 84 | var newTrackArt = new IPodImage(); 85 | newTrackArt.Create(_iPod, track, image); 86 | _childSections.Add(newTrackArt); 87 | 88 | foreach (var format in newTrackArt.Formats) 89 | { 90 | track.Artwork.Add(format); 91 | } 92 | track.ArtworkIdLink = newTrackArt.Id; 93 | } 94 | 95 | /// 96 | /// Enumerates each image in this ImageList. 97 | /// 98 | /// 99 | public IEnumerable Images() 100 | { 101 | foreach (var art in _childSections) 102 | { 103 | yield return art; 104 | } 105 | } 106 | 107 | internal void RemoveArtwork(IPodImage existingArt) 108 | { 109 | _childSections.Remove(existingArt); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/IdGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Clickwheel.Parsers.iTunesDB 5 | { 6 | internal class IdGenerator 7 | { 8 | private int _lastTrackId; 9 | private long _lastDBId; 10 | private int _lastPodcastGroupId = 1; 11 | 12 | private IPod _iPod; 13 | 14 | public IdGenerator(IPod iPod) 15 | { 16 | _iPod = iPod; 17 | 18 | _lastTrackId = 0; 19 | 20 | for (var i = 0; i < _iPod.Tracks.Count; i++) 21 | { 22 | if (_iPod.Tracks[i].Id > _lastTrackId) 23 | { 24 | _lastTrackId = _iPod.Tracks[i].Id; 25 | } 26 | 27 | if (_iPod.Tracks[i].DBId > _lastDBId) 28 | { 29 | _lastDBId = _iPod.Tracks[i].DBId; 30 | } 31 | } 32 | 33 | var listHeader = _iPod.ITunesDB.DatabaseRoot.GetChildSection( 34 | MHSDSectionType.PlaylistsV2 35 | ); 36 | if (listHeader != null) 37 | { 38 | var podcastsContainer = (PlaylistListV2Container)listHeader.GetListContainer(); 39 | var podcastsList = podcastsContainer.GetPlaylistsList(); 40 | 41 | var podcastsPlaylist = podcastsList.GetPlaylistByName("Podcasts"); 42 | if (podcastsPlaylist != null) 43 | { 44 | foreach (var item in podcastsPlaylist.Items()) 45 | { 46 | if (item.GroupId > _lastPodcastGroupId) 47 | { 48 | _lastPodcastGroupId = item.GroupId; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | public int GetNewTrackId() 56 | { 57 | _lastTrackId++; 58 | return _lastTrackId; 59 | } 60 | 61 | public long GetNewDBId() 62 | { 63 | _lastDBId++; 64 | return _lastDBId; 65 | } 66 | 67 | public string GetNewIPodFilePath(Track track, string fileExtension) 68 | { 69 | var r = new Random(); 70 | var folderNumber = "F" + r.Next(49).ToString("00"); 71 | 72 | string path; 73 | while (true) 74 | { 75 | if (track.MediaType == MediaType.Ringtone) 76 | { 77 | path = Path.Combine(_iPod.FileSystem.IPodControlPath, "Ringtones"); 78 | } 79 | else 80 | { 81 | path = Path.Combine( 82 | Path.Combine(_iPod.FileSystem.IPodControlPath, "Music"), 83 | folderNumber 84 | ); 85 | } 86 | path = Path.Combine(path, GetNewRandomFileName() + fileExtension); 87 | if (!_iPod.FileSystem.FileExists(path)) 88 | { 89 | break; 90 | } 91 | } 92 | 93 | return path.Substring(_iPod.DriveLetter.Length); 94 | } 95 | 96 | private string GetNewRandomFileName() 97 | { 98 | var path = ""; 99 | var r = new Random(); 100 | for (var i = 0; i < 4; i++) 101 | { 102 | var c = (char)r.Next(65, 90); 103 | var s = Convert.ToString(c); 104 | path += s; 105 | } 106 | path = "SP" + path; 107 | return path; 108 | } 109 | 110 | public uint GetNewArtworkId() 111 | { 112 | var newArtworkId = _iPod.ArtworkDB.NextImageId; 113 | _iPod.ArtworkDB.NextImageId++; 114 | return newArtworkId; 115 | } 116 | 117 | public int GetNewPodcastGroupId() 118 | { 119 | _lastPodcastGroupId++; 120 | return _lastPodcastGroupId; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Artwork/ImageAlbum.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Clickwheel.Parsers.iTunesDB; 4 | 5 | namespace Clickwheel.Parsers.Artwork 6 | { 7 | // Implements a MHBA entry in ArtworkDB / PhotoDB 8 | /// 9 | /// An Image Album 10 | /// 11 | public class ImageAlbum : BaseDatabaseElement 12 | { 13 | int _id; 14 | 15 | private List _dataObjects = new List(); 16 | private List _images = new List(); 17 | 18 | internal ImageAlbum() 19 | { 20 | _requiredHeaderSize = 24; 21 | } 22 | 23 | internal override void Read(IPod iPod, BinaryReader reader) 24 | { 25 | base.Read(iPod, reader); 26 | 27 | _identifier = reader.ReadChars(4); 28 | _headerSize = reader.ReadInt32(); 29 | 30 | ValidateHeader("mhba"); 31 | 32 | _sectionSize = reader.ReadInt32(); 33 | var dataObjectCount = reader.ReadInt32(); 34 | var imageCount = reader.ReadInt32(); 35 | _id = reader.ReadInt32(); 36 | 37 | base.ReadToHeaderEnd(reader); 38 | 39 | for (var i = 0; i < dataObjectCount; i++) 40 | { 41 | var mhod = new ArtworkStringMHOD(); 42 | mhod.Read(iPod, reader); 43 | _dataObjects.Add(mhod); 44 | } 45 | 46 | for (var i = 0; i < imageCount; i++) 47 | { 48 | var item = new ImageAlbumItem(); 49 | item.Read(iPod, reader); 50 | _images.Add(item); 51 | } 52 | } 53 | 54 | internal override void Write(BinaryWriter writer) 55 | { 56 | _sectionSize = GetSectionSize(); 57 | 58 | writer.Write(_identifier); 59 | writer.Write(_headerSize); 60 | writer.Write(_sectionSize); 61 | writer.Write(_dataObjects.Count); 62 | writer.Write(_images.Count); 63 | writer.Write(_id); 64 | writer.Write(_unusedHeader); 65 | 66 | for (var i = 0; i < _dataObjects.Count; i++) 67 | { 68 | _dataObjects[i].Write(writer); 69 | } 70 | 71 | for (var i = 0; i < _images.Count; i++) 72 | { 73 | _images[i].Write(writer); 74 | } 75 | } 76 | 77 | internal override int GetSectionSize() 78 | { 79 | var size = _headerSize; 80 | foreach (var mhod in _dataObjects) 81 | { 82 | size += mhod.GetSectionSize(); 83 | } 84 | 85 | foreach (var item in _images) 86 | { 87 | size += item.GetSectionSize(); 88 | } 89 | 90 | return size; 91 | } 92 | 93 | internal void ResolveImages(ImageList images) 94 | { 95 | foreach (var item in _images) 96 | { 97 | var art = images.GetArtById(item.ImageId); 98 | item.Artwork = art; 99 | } 100 | } 101 | 102 | /// 103 | /// Title of this Image Album 104 | /// 105 | public string Title 106 | { 107 | get 108 | { 109 | var name = base.GetDataElement(_dataObjects, MHODElementType.Title); 110 | if (string.IsNullOrEmpty(name)) 111 | { 112 | name = "Unnamed"; 113 | } 114 | 115 | return name; 116 | } 117 | } 118 | 119 | /// 120 | /// Number of images in this album 121 | /// 122 | public int ImageCount => _images.Count; 123 | 124 | public IEnumerable Images 125 | { 126 | get 127 | { 128 | foreach (var item in _images) 129 | { 130 | if (item.Artwork != null) 131 | { 132 | yield return item.Artwork; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /clickwheel-wordmark-on-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /clickwheel-wordmark-on-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Clickwheel/IPodBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Clickwheel 6 | { 7 | /// 8 | /// Provides static methods for backing up and restoring the iPod database. 9 | /// 10 | public static class IPodBackup 11 | { 12 | private static bool _backupPerformed; 13 | private static string _overrideBackupsFolder; 14 | private static int _numberBackupsToKeep = 1; 15 | private static bool _enableBackups = true; 16 | 17 | /// 18 | /// If not set, this defaults to [ApplicationData]\Clickwheel\Backups. 19 | /// 20 | public static string BackupsFolder 21 | { 22 | get => _overrideBackupsFolder; 23 | set => _overrideBackupsFolder = value; 24 | } 25 | 26 | /// 27 | /// Number of backup files to keep before deleting old files. 28 | /// Defaults to 1 29 | /// 30 | public static int NumberBackupsToKeep 31 | { 32 | get => _numberBackupsToKeep; 33 | set => _numberBackupsToKeep = value; 34 | } 35 | 36 | /// 37 | /// Enable/disable backup creation. By default this is set to true (Enabled) 38 | /// 39 | public static bool EnableBackups 40 | { 41 | get => _enableBackups; 42 | set => _enableBackups = value; 43 | } 44 | 45 | /// 46 | /// Will backup the iPod's database (iTunesDB, ArtworkDB files) if it hasnt been backed up this session already. 47 | /// 48 | internal static void BackupDatabase(IPod iPod) 49 | { 50 | if (_backupPerformed || (_enableBackups == false)) 51 | { 52 | return; 53 | } 54 | 55 | var backupFolder = GetBackupsFolder(iPod); 56 | 57 | if (!Directory.Exists(backupFolder)) 58 | { 59 | Directory.CreateDirectory(backupFolder); 60 | } 61 | else 62 | { 63 | var di = new DirectoryInfo(backupFolder); 64 | var backupFiles = new List(di.GetFiles("*DB_*.spbackup")); 65 | 66 | while (backupFiles.Count > (NumberBackupsToKeep - 1) * 2 && backupFiles.Count > 0) 67 | { 68 | var oldestFile = backupFiles[0]; 69 | foreach (var backupFile in backupFiles) 70 | { 71 | if (backupFile.CreationTime < oldestFile.CreationTime) 72 | { 73 | oldestFile = backupFile; 74 | } 75 | } 76 | oldestFile.Delete(); 77 | backupFiles.Remove(oldestFile); 78 | } 79 | } 80 | 81 | if (NumberBackupsToKeep > 0) 82 | { 83 | _backupPerformed = true; 84 | } 85 | } 86 | 87 | /// 88 | /// Returns a list of Clickwheel backup files (files called *.spbackup in backups folder) 89 | /// 90 | /// 91 | public static FileInfo[] GetBackups(IPod iPod) 92 | { 93 | var backupFolder = GetBackupsFolder(iPod); 94 | 95 | if (!Directory.Exists(backupFolder)) 96 | { 97 | return null; 98 | } 99 | else 100 | { 101 | var di = new DirectoryInfo(backupFolder); 102 | return di.GetFiles("ITunesDB_*.spbackup"); 103 | } 104 | } 105 | 106 | private static string GetBackupsFolder(IPod iPod) 107 | { 108 | //FirewireId should only ever be null for 3rd gen (old) iPods which dont support the SCSI device query 109 | var firewireId = iPod.DeviceInfo.FirewireId ?? ""; 110 | 111 | if (_overrideBackupsFolder != null) 112 | { 113 | return Path.Combine(_overrideBackupsFolder, firewireId); 114 | } 115 | else 116 | { 117 | return Path.Combine( 118 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 119 | "Clickwheel", 120 | "Backups", 121 | firewireId 122 | ); 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/ArtworkStringMHOD.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Clickwheel.Parsers.iTunesDB 5 | { 6 | enum StringEncodingType 7 | { 8 | Ascii = 0, 9 | UTF8 = 1, 10 | Unicode = 2 11 | } 12 | 13 | /// 14 | /// Implements an MHOD used in the ArtworkDB and PhotoDB 15 | /// 16 | class ArtworkStringMHOD : StringMHOD 17 | { 18 | private ushort _unk1x; 19 | private int _padding; 20 | private StringEncodingType _stringType; 21 | private int _unk3; 22 | private string _data; 23 | 24 | private int _actualPadding; 25 | 26 | internal ArtworkStringMHOD() 27 | { 28 | _requiredHeaderSize = 24; 29 | 30 | _headerSize = 24; 31 | _identifier = "mhod".ToCharArray(); 32 | _type = MHODElementType.Album; //3 33 | _stringType = StringEncodingType.Unicode; 34 | } 35 | 36 | #region IDatabaseElement Members 37 | 38 | internal override void Read(IPod iPod, BinaryReader reader) 39 | { 40 | var startOfElement = reader.BaseStream.Position; 41 | 42 | _identifier = reader.ReadChars(4); 43 | _headerSize = reader.ReadInt32(); 44 | 45 | ValidateHeader("mhod"); 46 | 47 | _sectionSize = reader.ReadInt32(); 48 | _type = reader.ReadUInt16(); 49 | _unk1x = reader.ReadUInt16(); 50 | 51 | _unk2 = reader.ReadInt32(); 52 | _padding = reader.ReadInt32(); 53 | 54 | ReadToHeaderEnd(reader); 55 | 56 | var dataLength = reader.ReadInt32(); 57 | _stringType = (StringEncodingType)reader.ReadInt32(); 58 | _unk3 = reader.ReadInt32(); 59 | var bytes = reader.ReadBytes(dataLength); 60 | 61 | _data = StringEncoding.GetString(bytes); 62 | 63 | _actualPadding = (int)((startOfElement + _sectionSize) - reader.BaseStream.Position); 64 | //Jump over padding section 65 | if (_actualPadding != 0) 66 | { 67 | reader.BaseStream.Seek(startOfElement + _sectionSize, SeekOrigin.Begin); 68 | } 69 | } 70 | 71 | internal override void Write(BinaryWriter writer) 72 | { 73 | _sectionSize = GetSectionSize(); 74 | 75 | var bytes = StringEncoding.GetBytes(_data); 76 | 77 | writer.Write(_identifier); 78 | writer.Write(_headerSize); 79 | writer.Write(_sectionSize); 80 | writer.Write((ushort)_type); 81 | writer.Write(_unk1x); 82 | writer.Write(_unk2); 83 | writer.Write(_padding); 84 | writer.Write(_unusedHeader); 85 | writer.Write(bytes.Length); 86 | writer.Write((int)_stringType); 87 | writer.Write(_unk3); 88 | writer.Write(bytes); 89 | 90 | //Jump over padding section 91 | writer.BaseStream.Seek(_actualPadding, SeekOrigin.Current); 92 | } 93 | 94 | internal override int GetSectionSize() 95 | { 96 | var dataLength = StringEncoding.GetByteCount(_data); 97 | return _headerSize + 12 + dataLength + _actualPadding; 98 | } 99 | 100 | #endregion 101 | 102 | public override string Data 103 | { 104 | get => _data; 105 | set 106 | { 107 | if (!value.StartsWith(":")) 108 | { 109 | _data = ":" + value; 110 | } 111 | else 112 | { 113 | _data = value; 114 | } 115 | } 116 | } 117 | 118 | internal void Create(string data) 119 | { 120 | _unusedHeader = new byte[_headerSize - _requiredHeaderSize]; 121 | Data = data; 122 | } 123 | 124 | private Encoding StringEncoding 125 | { 126 | get 127 | { 128 | if (_stringType == StringEncodingType.Unicode) 129 | { 130 | return Encoding.Unicode; 131 | } 132 | else if (_stringType == StringEncodingType.Ascii) 133 | { 134 | return Encoding.ASCII; 135 | } 136 | else 137 | { 138 | return Encoding.UTF8; 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/PodcastListAdapter.cs: -------------------------------------------------------------------------------- 1 | namespace Clickwheel.Parsers.iTunesDB 2 | { 3 | /// 4 | /// Writes the necessary entries for the Podcast playlist in the Podcast MHSD section. 5 | /// 6 | internal class PodcastListAdapter 7 | { 8 | private Playlist _playlist; 9 | private IPod _iPod; 10 | 11 | public PodcastListAdapter(IPod iPod, Playlist playlist) 12 | { 13 | _iPod = iPod; 14 | _playlist = playlist; 15 | _playlist.IsPodcastPlaylist = true; 16 | } 17 | 18 | public void AddTrack(Track track) 19 | { 20 | PlaylistItem trackItem = null; 21 | foreach (var item in _playlist.Items()) 22 | { 23 | if (item.Track == track) 24 | { 25 | trackItem = item; 26 | break; 27 | } 28 | } 29 | //If the track is in the list and has a valid group, dont need to do anything else. 30 | if (trackItem != null && trackItem.GroupId != 0) 31 | { 32 | return; 33 | } 34 | 35 | var parentItem = GetPodcastGroup(track.Album); 36 | 37 | if (trackItem == null) 38 | { 39 | trackItem = new PlaylistItem(); 40 | trackItem.Track = track; 41 | _playlist.AddItem(trackItem, -1); 42 | } 43 | trackItem.PodcastGroupParentId = parentItem.GroupId; 44 | trackItem.GroupId = _iPod.IdGenerator.GetNewPodcastGroupId(); 45 | } 46 | 47 | public void RemoveItem(PlaylistItem item) 48 | { 49 | _playlist.RemoveItem(item); 50 | var parentItem = GetPodcastGroup(item.PodcastGroupParentId); 51 | if (parentItem != null) 52 | { 53 | if (!PodcastGroupHasEntries(parentItem)) 54 | { 55 | _playlist.RemoveItem(parentItem); 56 | } 57 | } 58 | } 59 | 60 | private PlaylistItem GetPodcastGroup(string groupName) 61 | { 62 | foreach (var item in _playlist.Items()) 63 | { 64 | if (item.IsPodcastGroup) 65 | { 66 | if (item.PodcastGroupTitle == groupName) 67 | { 68 | return item; 69 | } 70 | } 71 | } 72 | return CreatePodcastGroup(groupName); 73 | } 74 | 75 | private PlaylistItem CreatePodcastGroup(string groupName) 76 | { 77 | var item = new PlaylistItem(); 78 | item.PodcastGroupTitle = groupName; 79 | item.IsPodcastGroup = true; 80 | item.PodcastGroupParentId = 0; 81 | item.GroupId = _iPod.IdGenerator.GetNewPodcastGroupId(); 82 | _playlist.AddItem(item, 0); 83 | return item; 84 | } 85 | 86 | private bool PodcastGroupHasEntries(PlaylistItem podcastGroup) 87 | { 88 | foreach (var item in _playlist.Items()) 89 | { 90 | if (item.PodcastGroupParentId == podcastGroup.GroupId) 91 | { 92 | return true; 93 | } 94 | } 95 | return false; 96 | } 97 | 98 | private PlaylistItem GetPodcastGroup(int groupId) 99 | { 100 | foreach (var item in _playlist.Items()) 101 | { 102 | if (item.GroupId == groupId) 103 | { 104 | return item; 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | internal void FollowChanges(Playlist otherPlaylist) 111 | { 112 | for (var count = 0; count < otherPlaylist.ItemCount; count++) 113 | { 114 | if (otherPlaylist[count] != null) 115 | { 116 | AddTrack(otherPlaylist[count]); 117 | } 118 | } 119 | 120 | for (var count = _playlist.ItemCount - 1; count >= 0; count--) 121 | { 122 | if (_playlist.ItemCount == 0) 123 | { 124 | break; 125 | } 126 | 127 | if (!_playlist.GetPlaylistItem(count).IsPodcastGroup) 128 | { 129 | if (!otherPlaylist.ContainsTrack(_playlist[count])) 130 | { 131 | RemoveItem(_playlist.GetPlaylistItem(count)); 132 | count = _playlist.ItemCount; //restart from top again 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/iTunesDBRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Clickwheel.Parsers.iTunesDB 6 | { 7 | /// 8 | /// Implements an MHBD section in the iTunesDB file 9 | /// 10 | public class iTunesDBRoot : BaseDatabaseElement 11 | { 12 | protected int _unk1; 13 | protected int _versionNumber; 14 | protected int _listContainerCount; 15 | protected ulong _id; 16 | protected byte[] _unk2; 17 | protected short _hashingScheme; 18 | 19 | internal List _childSections; 20 | 21 | public int VersionNumber => _versionNumber; 22 | 23 | internal int HashingScheme => _hashingScheme; 24 | 25 | public iTunesDBRoot() 26 | { 27 | _requiredHeaderSize = 50; 28 | _childSections = new List(); 29 | } 30 | 31 | #region IDatabaseElement Members 32 | 33 | internal override void Read(IPod iPod, BinaryReader reader) 34 | { 35 | base.Read(iPod, reader); 36 | _identifier = reader.ReadChars(4); 37 | _headerSize = reader.ReadInt32(); 38 | 39 | ValidateHeader("mhbd"); 40 | 41 | _sectionSize = reader.ReadInt32(); 42 | _unk1 = reader.ReadInt32(); 43 | _versionNumber = reader.ReadInt32(); 44 | _listContainerCount = reader.ReadInt32(); 45 | _id = reader.ReadUInt64(); 46 | _unk2 = reader.ReadBytes(16); 47 | _hashingScheme = reader.ReadInt16(); 48 | 49 | this.ReadToHeaderEnd(reader); 50 | 51 | while (reader.BaseStream.Position != reader.BaseStream.Length) 52 | { 53 | var containerHeader = new ListContainerHeader(); 54 | containerHeader.Read(iPod, reader); 55 | _childSections.Add(containerHeader); 56 | } 57 | } 58 | 59 | internal override void Write(BinaryWriter writer) 60 | { 61 | _sectionSize = GetSectionSize(); 62 | 63 | writer.Write(_identifier); 64 | writer.Write(_headerSize); 65 | writer.Write(_sectionSize); 66 | writer.Write(_unk1); 67 | writer.Write(_versionNumber); 68 | writer.Write(_listContainerCount); 69 | //really this should be _childSections.Count, but have observed some 70 | //dbs with wrong count from iTunes, updating it means we fail compatibility test with SourceDoesntMatchOutput 71 | 72 | writer.Write(_id); 73 | writer.Write(_unk2); 74 | writer.Write(_hashingScheme); 75 | writer.Write(_unusedHeader); 76 | 77 | for (var i = 0; i < _childSections.Count; i++) 78 | { 79 | _childSections[i].Write(writer); 80 | } 81 | } 82 | 83 | internal override int GetSectionSize() 84 | { 85 | var size = _headerSize; 86 | for (var i = 0; i < _childSections.Count; i++) 87 | { 88 | size += _childSections[i].GetSectionSize(); 89 | } 90 | return size; 91 | } 92 | 93 | #endregion 94 | 95 | internal ListContainerHeader GetChildSection(MHSDSectionType type) 96 | { 97 | for (var i = 0; i < _childSections.Count; i++) 98 | { 99 | if (_childSections[i].Type == type) 100 | { 101 | return _childSections[i]; 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | /// 108 | /// Gets the PlaylistList, or PlaylistV2 if Playlist container doesn't exist 109 | /// 110 | /// 111 | public PlaylistList GetPlaylistList() 112 | { 113 | if (GetChildSection(MHSDSectionType.Playlists) != null) 114 | { 115 | var playlistsContainer = (PlaylistListContainer)GetChildSection( 116 | MHSDSectionType.Playlists 117 | ) 118 | .GetListContainer(); 119 | return playlistsContainer.GetPlaylistsList(); 120 | } 121 | 122 | if (GetChildSection(MHSDSectionType.PlaylistsV2) != null) 123 | { 124 | var playlistsContainer = (PlaylistListV2Container)GetChildSection( 125 | MHSDSectionType.PlaylistsV2 126 | ) 127 | .GetListContainer(); 128 | return playlistsContainer.GetPlaylistsList(); 129 | } 130 | 131 | throw new Exception("iTunesDB Playlist container not found"); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Clickwheel/DebugLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace Clickwheel 7 | { 8 | /// 9 | /// Utility class to log events and exceptions to a file. Clickwheel will write some events to this log if enabled. User applications may 10 | /// also use this to log non-Clickwheel events. 11 | /// 12 | class TraceLogger : TraceListener 13 | { 14 | private StreamWriter _file; 15 | private object _lockObject; 16 | 17 | public TraceLogger(object locker) 18 | { 19 | _lockObject = locker; 20 | } 21 | 22 | public void SetFileStream(StreamWriter stream) 23 | { 24 | _file = stream; 25 | } 26 | 27 | public override void Write(string message) 28 | { 29 | if (_file == null) 30 | { 31 | return; 32 | } 33 | 34 | lock (_lockObject) 35 | { 36 | _file.Write(message); 37 | _file.Flush(); 38 | } 39 | } 40 | 41 | public override void WriteLine(string message) 42 | { 43 | if (_file == null) 44 | { 45 | return; 46 | } 47 | 48 | lock (_lockObject) 49 | { 50 | _file.WriteLine(message); 51 | _file.Flush(); 52 | } 53 | } 54 | } 55 | 56 | /// 57 | /// Utility class to log Trace.Write/WriteLine events and exceptions to a file. 58 | /// Clickwheel will write some events to this log if enabled. User applications may also use this to log non-Clickwheel events. 59 | /// 60 | public static class DebugLogger 61 | { 62 | private static bool _isLogging; 63 | private static StreamWriter _file; 64 | private static TraceLogger _traceLogger; 65 | private static object _lockObject; 66 | 67 | static DebugLogger() 68 | { 69 | _lockObject = new object(); 70 | _traceLogger = new TraceLogger(_lockObject); 71 | Trace.Listeners.Add(_traceLogger); 72 | } 73 | 74 | /// 75 | /// Start logging. 76 | /// 77 | /// 78 | public static void StartLogging(string filename) 79 | { 80 | //Make sure we aren't already logging. 81 | StopLogging(); 82 | 83 | try 84 | { 85 | if (File.Exists(filename)) 86 | { 87 | File.Delete(filename); 88 | } 89 | 90 | _file = File.CreateText(filename); 91 | _isLogging = true; 92 | _traceLogger.SetFileStream(_file); 93 | 94 | var libFileName = Assembly.GetExecutingAssembly().GetModules()[ 95 | 0 96 | ].FullyQualifiedName; 97 | Trace.WriteLine("==============================="); 98 | Trace.WriteLine( 99 | "Clickwheel Version: " + FileVersionInfo.GetVersionInfo(libFileName).FileVersion 100 | ); 101 | Trace.WriteLine("==============================="); 102 | Trace.WriteLine(Environment.OSVersion.VersionString); 103 | } 104 | catch (Exception ex) 105 | { 106 | LogException(ex); 107 | } 108 | } 109 | 110 | /// 111 | /// Stop all logging. 112 | /// 113 | public static void StopLogging() 114 | { 115 | Trace.WriteLine("Logging stopped"); 116 | _traceLogger.SetFileStream(null); 117 | if (_file != null) 118 | { 119 | _file.Close(); 120 | _file = null; 121 | _isLogging = false; 122 | } 123 | } 124 | 125 | /// 126 | /// Log an Exception. Will be prefaced with 'Exception: ' 127 | /// 128 | /// 129 | public static void LogException(Exception ex) 130 | { 131 | lock (_lockObject) 132 | { 133 | if (_isLogging) 134 | { 135 | Debug.WriteLine("Exception: " + ex.Message); 136 | _file.WriteLine("Exception: " + ex); 137 | _file.Flush(); 138 | } 139 | } 140 | } 141 | 142 | /// 143 | /// Log an Exception. Will be prefaced with 'Unhandled Exception: ' 144 | /// 145 | /// 146 | public static void LogUnhandledException(object ex) 147 | { 148 | lock (_lockObject) 149 | { 150 | if (_isLogging) 151 | { 152 | _file.WriteLine("Unhandled Exception: " + ex); 153 | _file.Flush(); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/Base/BaseDatabase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Clickwheel.Exceptions; 4 | 5 | namespace Clickwheel.Parsers 6 | { 7 | internal abstract class BaseDatabase 8 | { 9 | public event EventHandler DatabaseWritten; 10 | 11 | protected CompatibilityType _compatibility; 12 | protected IPod _iPod; 13 | protected string _databaseFilePath; 14 | 15 | public IPod iPod => _iPod; 16 | 17 | public CompatibilityType Compatibility 18 | { 19 | get => _compatibility; 20 | set => _compatibility = value; 21 | } 22 | 23 | public abstract int Version { get; } 24 | public abstract void Parse(); 25 | public abstract void Save(); 26 | public abstract bool IsDirty { get; } 27 | 28 | protected void ReadDatabase(BaseDatabaseElement root) 29 | { 30 | var parseFilePath = GetParseFileName(); 31 | 32 | var fs = new FileStream(parseFilePath, FileMode.Open, FileAccess.Read); 33 | var reader = new BinaryReader(fs); 34 | 35 | try 36 | { 37 | root.Read(iPod, reader); 38 | reader.Close(); 39 | _compatibility = TestCompatibility(parseFilePath, root); 40 | } 41 | catch (Exception ex) 42 | { 43 | DebugLogger.LogException(ex); 44 | var message = 45 | $"The iPod database '{Path.GetFileName(_databaseFilePath)}' could not be read. Please run iTunes with your iPod connected, then try again. (Error at 0x{reader.BaseStream.Position.ToString("X")})"; 46 | throw new ParseException(message, ex); 47 | } 48 | finally 49 | { 50 | reader.Close(); 51 | CleanUpParseFile(parseFilePath); 52 | } 53 | } 54 | 55 | protected void WriteDatabase(BaseDatabaseElement root) 56 | { 57 | var tempDB = Path.GetTempFileName(); 58 | var fs = new FileStream(tempDB, FileMode.Create, FileAccess.ReadWrite); 59 | var writer = new BinaryWriter(fs); 60 | root.Write(writer); 61 | writer.Flush(); 62 | DoActionOnWriteDatabase(fs); 63 | 64 | if (fs.CanWrite) 65 | { 66 | writer.Flush(); 67 | } 68 | writer.Close(); 69 | 70 | //overwrite real database with temp 71 | _iPod.FileSystem.CopyFileToDevice(tempDB, _databaseFilePath); 72 | 73 | if (DatabaseWritten != null) 74 | { 75 | DatabaseWritten(this, null); 76 | } 77 | } 78 | 79 | public virtual void DoActionOnWriteDatabase(FileStream fileStream) { } 80 | 81 | public virtual void AssertIsWritable() 82 | { 83 | string msg; 84 | switch (_compatibility) 85 | { 86 | case CompatibilityType.NotWritable: 87 | msg = 88 | $"Your iPod ({_iPod.DeviceInfo.Family}, database version {Version}) is not writable. All iPod update features are disabled, but you can still copy files to your computer."; 89 | throw new UnsupportedIPodException(msg); 90 | case CompatibilityType.UnsupportedNewDeviceOrFirmware: 91 | msg = 92 | "Looks like you have a new iPod! This version of Clickwheel does not fully support it yet. You can only copy files from your iPod to your computer."; 93 | throw new UnsupportedIPodException(msg); 94 | case CompatibilityType.SourceDoesntMatchOutput: 95 | msg = 96 | "The iPod database failed to pass the Clickwheel compatibility test. All iPod update features are disabled. Upgrading the iPod to the latest iTunes version may fix the issue."; 97 | throw new UnsupportedITunesVersionException(msg, _compatibility); 98 | } 99 | } 100 | 101 | internal CompatibilityType TestCompatibility(string dbFilePath, BaseDatabaseElement root) 102 | { 103 | var tempDB = Path.GetTempFileName(); 104 | var fs = new FileStream(tempDB, FileMode.Create, FileAccess.Write); 105 | var writer = new BinaryWriter(fs); 106 | root.Write(writer); 107 | writer.Close(); 108 | return Helpers.TestCompatibility(dbFilePath, tempDB); 109 | } 110 | 111 | protected string GetParseFileName() 112 | { 113 | if (_iPod.FileSystem.ParseDbFilesLocally) 114 | { 115 | var parseFilePath = Path.GetTempFileName(); 116 | _iPod.FileSystem.CopyFileFromDevice(_databaseFilePath, parseFilePath); 117 | return parseFilePath; 118 | } 119 | else 120 | { 121 | return _databaseFilePath; 122 | } 123 | } 124 | 125 | protected void CleanUpParseFile(string parseFileUsed) 126 | { 127 | if (_iPod.FileSystem.ParseDbFilesLocally) 128 | { 129 | File.Delete(parseFileUsed); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | Clickwheel is a modern cross-platform iPod management API for .NET. 7 | 8 | ## Installing 9 | 10 | Clickwheel is available via NuGet. 11 | 12 | | Package Name | Version (NuGet) | 13 | | ------------ | --------------- | 14 | | `Clickwheel` | [![NuGet](https://img.shields.io/nuget/v/Clickwheel.svg)](https://www.nuget.org/packages/Clickwheel/) | 15 | 16 | ## Usage 17 | 18 | ```csharp 19 | using Clickwheel; 20 | 21 | var ipod = IPod.GetConnectedIPod(); 22 | var track = new NewTrack 23 | { 24 | FilePath = "01 Run Away With Me.m4a", 25 | Album = "E•MO•TION", 26 | Artist = "Carly Rae Jepsen", 27 | AlbumArtist = "Carly Rae Jepsen", 28 | Title = "Run Away With Me", 29 | IsVideo = false, 30 | ArtworkFile = "album.jpg", 31 | Year = 2015, 32 | DiscNumber = 1, 33 | TotalDiscCount = 1, 34 | TrackNumber = 1, 35 | AlbumTrackCount = 12, 36 | Genre = "Pop", 37 | }; 38 | 39 | var addedTrack = ipod.Tracks.Add(track); 40 | addedTrack.Rating = new IPodRating(5); 41 | 42 | var playlist = ipod.Playlists.Add("National Anthems"); 43 | playlist.AddTrack(addedTrack); 44 | 45 | // Write changes to iPod 46 | ipod.SaveChanges(); 47 | ``` 48 | 49 | ## Compatibility 50 | 51 | | Family | Generation | Supported | Supported Firmware Version | 52 | | ------------ | ---------- | ----------------- | --------------------------------- | 53 | | iPod | 1st | Supported | | 54 | | | 2nd | Supported | | 55 | | | 3rd | Supported | | 56 | | | 4th | Supported | 3.1.1 (Monochrome), 1.2.1 (Color) | 57 | | | 5th | Supported | 1.3 | 58 | | | 6th | Supported | 1.1.2 | 59 | | iPod mini | 1st | Supported | | 60 | | | 2nd | Supported | 1.4.1 | 61 | | iPod nano | 1st | Supported | 1.3.1 | 62 | | | 2nd | Supported | 1.1.3 | 63 | | | 3rd | Supported | 1.1.3 | 64 | | | 4th | Supported | 1.0.4 | 65 | | | 5th | Supported | 1.0.2 | 66 | | | 6th | Not Supported[^1] | | 67 | | | 7th | Not Supported[^1] | | 68 | | iPod shuffle | 1st | Supported | 1.1.5 | 69 | | | 2nd | Supported | 1.0.4 | 70 | | | 3rd | Not Supported[^2] | | 71 | | | 4th | Not Supported[^2] | | 72 | | iPod touch | All | Not Supported[^3] | | 73 | 74 | 75 | [^1]: Uses [HashAB](#hashab) 76 | [^2]: VoiceOver generation currently unsupported 77 | [^3]: Clickwheel doesn't support iOS devices 78 | 79 | ## Extended SysInfo 80 | 81 | iPods that support album artwork store some additional system attributes which are accessible via either a SCSI INQUIRY command or a USB Control Transfer (starting with the 6th generation iPod, the 3rd generation iPod nano, and the 3rd generation iPod shuffle). Clickwheel requires this data to be present in a file named `SysInfoExtended` in the iPod's `iPod_Control/Device` folder for proper album artwork support and database hashing. How this file is created depends on your operating system. 82 | 83 | ### Windows 84 | 85 | On Windows, you can use [Clickwheel Device Helper](https://github.com/dstaley/clickwheel/blob/main/src/Clickwheel.DeviceHelper.GUI/README.md) to create the `SysInfoExtended` file for all attached iPods. 86 | 87 | ### Linux 88 | 89 | On Linux, you can use `ipod-read-sysinfo-extended` to obtain the `SysInfoExtended` file. This command is provided by the following packages: 90 | 91 | | Distro | Package | 92 | | ------------- | --------------- | 93 | | Arch | `libgpod` | 94 | | Debian/Ubuntu | `libgpod4` | 95 | | Fedora | `libgpod` | 96 | | openSUSE | `libgpod-tools` | 97 | 98 | ### macOS 99 | 100 | Unfortunately, modern versions of macOS do not support issuing SCSI INQUIRY requests to attached USB devices from userspace. If you're using an iPod that supports retrieving the extended SysInfo via a USB Control Transfer (any iPod released in 2007 or later), you can use [this](https://github.com/dstaley/ipod-read-sysinfo-extended-macos) program to write the `SysInfoExtended` file. Otherwise, you'll need to use a Windows or Linux device. 101 | 102 | ## HashAB 103 | 104 | HashAB is the name given to the as-of-yet unbroken database hashing scheme used on the 6th and 7th generation iPod nanos. Clickwheel does not support devices that use HashAB. 105 | 106 | ## Acknowledgements 107 | 108 | Clickwheel is a modern update of [sharepod-lib](https://github.com/dstaley/sharepod-lib), originally written by Jeffrey Harris. -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesCDB/ITunesCDBRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using Clickwheel.Parsers.iTunesDB; 6 | 7 | namespace Clickwheel.Parsers.iTunesCDB 8 | { 9 | /// 10 | /// Implements an MHBD section in the iTunesCDB file. The only difference between this file and the regular iTunesDB is the 11 | /// content is zlib compressed. 12 | /// 13 | class ITunesCDBRoot : iTunesDBRoot 14 | { 15 | private List _dirtyTracks; 16 | private List _dirtyPlaylists; 17 | 18 | public ITunesCDBRoot() : base() { } 19 | 20 | #region IDatabaseElement Members 21 | 22 | internal override void Read(IPod iPod, BinaryReader reader) 23 | { 24 | _iPod = iPod; 25 | _identifier = reader.ReadChars(4); 26 | _headerSize = reader.ReadInt32(); 27 | 28 | ValidateHeader("mhbd"); 29 | 30 | _sectionSize = reader.ReadInt32(); 31 | _unk1 = reader.ReadInt32(); 32 | _versionNumber = reader.ReadInt32(); 33 | _listContainerCount = reader.ReadInt32(); 34 | _id = reader.ReadUInt64(); 35 | _unk2 = reader.ReadBytes(16); 36 | _hashingScheme = reader.ReadInt16(); 37 | 38 | ReadToHeaderEnd(reader); 39 | 40 | var data = reader.ReadBytes(_sectionSize - _headerSize); 41 | 42 | using (var decompressed = new MemoryStream()) 43 | using (var compressed = new MemoryStream(data)) 44 | using (var decompressor = new ZLibStream(compressed, CompressionMode.Decompress)) 45 | { 46 | decompressor.CopyTo(decompressed); 47 | data = decompressed.ToArray(); 48 | } 49 | 50 | var contentsReader = new BinaryReader(new MemoryStream(data)); 51 | 52 | while (contentsReader.BaseStream.Position != contentsReader.BaseStream.Length) 53 | { 54 | var containerHeader = new ListContainerHeader(); 55 | containerHeader.Read(iPod, contentsReader); 56 | _childSections.Add(containerHeader); 57 | } 58 | 59 | contentsReader.Close(); 60 | reader.Close(); 61 | 62 | //Get all dirty tracks and register callback for successful iTunesDB write. 63 | _iPod.ITunesDB.DatabaseWritten += new EventHandler(ITunesDB_DatabaseWritten); 64 | } 65 | 66 | internal override void Write(BinaryWriter writer) 67 | { 68 | if (_iPod.Tracks != null) 69 | { 70 | _dirtyTracks = new List(); 71 | foreach (var t in _iPod.Tracks) 72 | { 73 | if (t.IsDirty) 74 | { 75 | _dirtyTracks.Add(t); 76 | } 77 | } 78 | _dirtyPlaylists = new List(); 79 | foreach (var p in _iPod.Playlists) 80 | { 81 | if (p.IsDirty) 82 | { 83 | _dirtyPlaylists.Add(p); 84 | } 85 | } 86 | } 87 | 88 | byte[] data; 89 | var contents = new MemoryStream(); 90 | var contentsWriter = new BinaryWriter(contents); 91 | 92 | for (var i = 0; i < _childSections.Count; i++) 93 | { 94 | _childSections[i].Write(contentsWriter); 95 | } 96 | 97 | using (var compressed = new MemoryStream()) 98 | { 99 | using (var compressor = new ZLibStream(compressed, CompressionLevel.Fastest)) 100 | { 101 | compressor.Write(contents.GetBuffer(), 0, (int)contents.Length); 102 | } 103 | data = compressed.ToArray(); 104 | } 105 | contentsWriter.Close(); 106 | 107 | _sectionSize = GetSectionSize() + data.Length; 108 | 109 | writer.Write(_identifier); 110 | writer.Write(_headerSize); 111 | writer.Write(_sectionSize); 112 | writer.Write(_unk1); 113 | writer.Write(_versionNumber); 114 | writer.Write(_listContainerCount); 115 | //really this should be _childSections.Count, but have observed some 116 | //dbs with wrong count from iTunes, updating it means we fail compatibility test with SourceDoesntMatchOutput 117 | 118 | writer.Write(_id); 119 | writer.Write(_unk2); 120 | writer.Write(_hashingScheme); 121 | writer.Write(_unusedHeader); 122 | writer.Write(data); 123 | } 124 | 125 | void ITunesDB_DatabaseWritten(object sender, EventArgs e) 126 | { 127 | SqliteTables sqlTables = null; 128 | 129 | switch (_iPod.DeviceInfo.Family) 130 | { 131 | case IPodFamily.iPod_Nano_Gen5: 132 | sqlTables = new SqliteTables_Nano5G(_iPod); 133 | break; 134 | } 135 | 136 | sqlTables.UpdateTracks(_dirtyTracks); 137 | sqlTables.UpdatePlaylists(_dirtyPlaylists); 138 | sqlTables.Save(); 139 | sqlTables.UpdateLocationsCbk(); 140 | } 141 | 142 | internal override int GetSectionSize() 143 | { 144 | return _headerSize; 145 | } 146 | 147 | #endregion 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/MusicDatabase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using Clickwheel.DatabaseHash; 5 | using Clickwheel.Exceptions; 6 | using Clickwheel.Parsers.iTunesCDB; 7 | using Clickwheel.Parsers.iTunesDB; 8 | 9 | namespace Clickwheel.Parsers 10 | { 11 | internal class MusicDatabase : BaseDatabase 12 | { 13 | internal iTunesDBRoot DatabaseRoot; 14 | internal TrackList TracksList; 15 | internal PlaylistList PlaylistsList; 16 | 17 | public MusicDatabase(IPod iPod) 18 | { 19 | _iPod = iPod; 20 | var fs = _iPod.FileSystem; 21 | if (fs.FileExists(fs.CombinePath(fs.ITunesFolderPath, "iTunesCDB"))) 22 | { 23 | _databaseFilePath = fs.CombinePath(fs.ITunesFolderPath, "iTunesCDB"); 24 | } 25 | else 26 | { 27 | _databaseFilePath = fs.CombinePath(fs.ITunesFolderPath, "iTunesDB"); 28 | } 29 | } 30 | 31 | public override int Version => DatabaseRoot.VersionNumber; 32 | 33 | internal int HashingScheme => DatabaseRoot.HashingScheme; 34 | 35 | public override void Parse() 36 | { 37 | if (!_iPod.FileSystem.FileExists(_databaseFilePath)) 38 | { 39 | throw new InvalidIPodDriveException( 40 | "iPod database not found in " + _databaseFilePath 41 | ); 42 | } 43 | 44 | if (_iPod.FileSystem.GetFileLength(_databaseFilePath) == 0) 45 | { 46 | throw new InvalidIPodDriveException( 47 | $"Database file at {_databaseFilePath} is empty. Please run iTunes with your iPod connected, then try again." 48 | ); 49 | } 50 | 51 | if (_iPod.FileSystem.FileExists(_iPod.FileSystem.ITunesLockPath)) 52 | { 53 | try 54 | { 55 | _iPod.FileSystem.DeleteFile(_iPod.FileSystem.ITunesLockPath); 56 | } 57 | catch 58 | { 59 | throw new ITunesLockException(_iPod.FileSystem.ITunesLockPath); 60 | } 61 | } 62 | 63 | var stopwatch = new Stopwatch(); 64 | stopwatch.Start(); 65 | 66 | if (_databaseFilePath.EndsWith("iTunesCDB")) 67 | { 68 | DatabaseRoot = new ITunesCDBRoot(); 69 | } 70 | else 71 | { 72 | DatabaseRoot = new iTunesDBRoot(); 73 | } 74 | 75 | ReadDatabase(DatabaseRoot); 76 | 77 | stopwatch.Stop(); 78 | Trace.WriteLine( 79 | $"MusicDatabase: {_compatibility}, version {DatabaseRoot.VersionNumber}" 80 | ); 81 | Trace.WriteLine($"MusicDatabase: HashingScheme {DatabaseRoot.HashingScheme}"); 82 | 83 | Debug.WriteLine($"MusicDatabase: Parsed in {stopwatch.ElapsedMilliseconds} msec"); 84 | 85 | var tracksContainer = (TrackListContainer)DatabaseRoot 86 | .GetChildSection(MHSDSectionType.Tracks) 87 | .GetListContainer(); 88 | TracksList = tracksContainer.GetTrackList(); 89 | 90 | PlaylistsList = DatabaseRoot.GetPlaylistList(); 91 | PlaylistsList.ResolveTracks(); 92 | } 93 | 94 | public override void Save() 95 | { 96 | AssertIsWritable(); 97 | Debug.WriteLine("Saving MusicDatabase " + DateTime.Now); 98 | IPodBackup.BackupDatabase(_iPod); 99 | 100 | PlaylistListV2Container playlistV2Container = null; 101 | if (DatabaseRoot.GetChildSection(MHSDSectionType.PlaylistsV2) != null) 102 | { 103 | playlistV2Container = (PlaylistListV2Container)DatabaseRoot 104 | .GetChildSection(MHSDSectionType.PlaylistsV2) 105 | .GetListContainer(); 106 | var playlistV2List = playlistV2Container.GetPlaylistsList(); 107 | if (this.PlaylistsList != playlistV2List) 108 | { 109 | //if we aren't already using the V2 playlist, sync it up here 110 | playlistV2List.FollowChanges(this.PlaylistsList); 111 | } 112 | } 113 | 114 | PlaylistsList[0].ReIndex(); 115 | 116 | WriteDatabase(DatabaseRoot); 117 | } 118 | 119 | public override void DoActionOnWriteDatabase(FileStream stream) 120 | { 121 | if (DatabaseRoot.VersionNumber >= 25) 122 | { 123 | DatabaseHasher.Hash(stream, _iPod); 124 | } 125 | } 126 | 127 | #region Properties 128 | 129 | public override bool IsDirty 130 | { 131 | get 132 | { 133 | if (TracksList.IsDirty || PlaylistsList.IsDirty) 134 | { 135 | return true; 136 | } 137 | 138 | foreach (var t in TracksList) 139 | { 140 | if (t.IsDirty) 141 | { 142 | return true; 143 | } 144 | } 145 | foreach (var p in PlaylistsList) 146 | { 147 | if (p.IsDirty) 148 | { 149 | return true; 150 | } 151 | } 152 | return false; 153 | } 154 | } 155 | 156 | #endregion 157 | 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/MHOD/MenuIndexMHOD.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Clickwheel.Exceptions; 5 | 6 | namespace Clickwheel.Parsers.iTunesDB 7 | { 8 | internal enum MenuIndexType 9 | { 10 | Title = 3, 11 | Album_Track_Title = 4, 12 | Artist_Album_Track_Title = 5, 13 | Genre_Artist_Album_Track_Title = 7 14 | } 15 | 16 | /// 17 | /// Implements a Type-52 MHOD. This is a fast lookup table the iPod uses for Artist, Album, Genre menus. 18 | /// 19 | class MenuIndexMHOD : BaseMHODElement 20 | { 21 | int _indexType; 22 | byte[] padding = new byte[40]; 23 | List _indexes; 24 | 25 | public MenuIndexMHOD() : base() 26 | { 27 | _type = MHODElementType.MenuIndexTable; 28 | } 29 | 30 | public MenuIndexMHOD(MenuIndexType indexType) : this() 31 | { 32 | _indexType = (int)indexType; 33 | } 34 | 35 | internal override void Read(IPod iPod, BinaryReader reader) 36 | { 37 | _indexType = reader.ReadInt32(); 38 | var nbrEntries = reader.ReadInt32(); 39 | padding = reader.ReadBytes(40); 40 | 41 | _indexes = new List(); 42 | 43 | for (var i = 0; i < nbrEntries; i++) 44 | { 45 | _indexes.Add(reader.ReadInt32()); 46 | } 47 | } 48 | 49 | internal override void Write(BinaryWriter writer) 50 | { 51 | base.Write(writer); 52 | 53 | writer.Write(_indexType); 54 | writer.Write(_indexes.Count); 55 | 56 | writer.Write(padding); 57 | 58 | foreach (var i in _indexes) 59 | { 60 | writer.Write(i); 61 | } 62 | } 63 | 64 | internal override int GetSectionSize() 65 | { 66 | return _headerSize + 48 + _indexes.Count * 4; 67 | } 68 | 69 | public void ReIndex(List tracks) 70 | { 71 | if (!IsSupported) 72 | { 73 | throw new BaseClickwheelException($"MenuIndexType: {_indexType} is not supported for reindexing."); 74 | } 75 | 76 | var indexType = (MenuIndexType)_indexType; 77 | 78 | switch (indexType) 79 | { 80 | case MenuIndexType.Title: 81 | tracks.Sort(new TitleComparer()); 82 | break; 83 | case MenuIndexType.Album_Track_Title: 84 | tracks.Sort(new AlbumTrackTitleComparer()); 85 | break; 86 | case MenuIndexType.Artist_Album_Track_Title: 87 | tracks.Sort(new ArtistAlbumTrackTitleComparer()); 88 | break; 89 | case MenuIndexType.Genre_Artist_Album_Track_Title: 90 | tracks.Sort(new GenreArtistAlbumTrackTitleComparer()); 91 | break; 92 | } 93 | 94 | _indexes = new List(); 95 | foreach (var t in tracks) 96 | { 97 | _indexes.Add(t.Index); 98 | } 99 | 100 | for (var i = 0; i > _indexes.Count; i++) 101 | { 102 | if (!_indexes.Contains(i)) { } 103 | else if ( 104 | _indexes 105 | .FindAll( 106 | delegate(int i2) 107 | { 108 | return i2 == i; 109 | } 110 | ) 111 | .Count > 1 112 | ) { } 113 | } 114 | } 115 | 116 | public bool IsSupported => Enum.IsDefined(typeof(MenuIndexType), _indexType); 117 | 118 | public int IndexType => _indexType; 119 | } 120 | 121 | internal class TitleComparer : IComparer 122 | { 123 | #region IComparer Members 124 | 125 | public int Compare(Track x, Track y) 126 | { 127 | var result = x.Title.CompareTo(y.Title); 128 | return result; 129 | } 130 | 131 | #endregion 132 | } 133 | 134 | internal class AlbumTrackTitleComparer : IComparer 135 | { 136 | #region IComparer Members 137 | 138 | public int Compare(Track x, Track y) 139 | { 140 | var result = (x.Album ?? string.Empty).CompareTo(y.Album); 141 | if (result != 0) 142 | { 143 | return result; 144 | } 145 | 146 | result = x.TrackNumber.CompareTo(y.TrackNumber); 147 | if (result != 0) 148 | { 149 | return result; 150 | } 151 | 152 | result = (x.Title ?? string.Empty).CompareTo(y.Title); 153 | return result; 154 | } 155 | 156 | #endregion 157 | } 158 | 159 | internal class ArtistAlbumTrackTitleComparer : IComparer 160 | { 161 | #region IComparer Members 162 | 163 | public int Compare(Track x, Track y) 164 | { 165 | var result = (x.SortArtist ?? string.Empty).CompareTo(y.SortArtist); 166 | if (result != 0) 167 | { 168 | return result; 169 | } 170 | 171 | result = (x.Album ?? string.Empty).CompareTo(y.Album); 172 | if (result != 0) 173 | { 174 | return result; 175 | } 176 | 177 | result = x.TrackNumber.CompareTo(y.TrackNumber); 178 | if (result != 0) 179 | { 180 | return result; 181 | } 182 | 183 | result = (x.Title ?? string.Empty).CompareTo(y.Title); 184 | return result; 185 | } 186 | 187 | #endregion 188 | } 189 | 190 | internal class GenreArtistAlbumTrackTitleComparer : IComparer 191 | { 192 | #region IComparer Members 193 | 194 | public int Compare(Track x, Track y) 195 | { 196 | var result = (x.Genre ?? string.Empty).CompareTo(y.Genre); 197 | if (result != 0) 198 | { 199 | return result; 200 | } 201 | 202 | result = (x.SortArtist ?? string.Empty).CompareTo(y.SortArtist); 203 | if (result != 0) 204 | { 205 | return result; 206 | } 207 | 208 | result = (x.Album ?? string.Empty).CompareTo(y.Album); 209 | if (result != 0) 210 | { 211 | return result; 212 | } 213 | 214 | result = x.TrackNumber.CompareTo(y.TrackNumber); 215 | if (result != 0) 216 | { 217 | return result; 218 | } 219 | 220 | result = (x.Title ?? string.Empty).CompareTo(y.Title); 221 | return result; 222 | } 223 | 224 | #endregion 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Clickwheel.DeviceHelper.GUI/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Security.Principal; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using System.Windows.Interop; 11 | using System.Windows.Media.Imaging; 12 | using Windows.Win32; 13 | using Windows.Win32.Foundation; 14 | using Windows.Win32.Graphics.Dwm; 15 | using Windows.Win32.UI.Shell; 16 | using Wpf.Ui.Common; 17 | 18 | namespace Clickwheel.DeviceHelper.GUI 19 | { 20 | public partial class MainWindow : Window 21 | { 22 | public MainWindow() 23 | { 24 | InitializeComponent(); 25 | ActivateDarkMode(); 26 | 27 | if (IsAdministrator()) 28 | { 29 | Heading.Text = "Clickwheel Device Helper"; 30 | Subhead.Text = 31 | "This app will store a SysInfoExtended file on each attached iPod, allowing you to use applications that use the Clickwheel library."; 32 | ButtonText.Text = "Start"; 33 | } 34 | else 35 | { 36 | Heading.Text = "Relaunch as Administrator?"; 37 | Subhead.Text = "Clickwheel Device Helper needs to be run as Administrator in order to access your iPod."; 38 | ButtonText.Text = "Run as Administrator"; 39 | 40 | var sii = new SHSTOCKICONINFO 41 | { 42 | cbSize = (UInt32)Marshal.SizeOf(typeof(SHSTOCKICONINFO)) 43 | }; 44 | 45 | Marshal.ThrowExceptionForHR(PInvoke.SHGetStockIconInfo(SHSTOCKICONID.SIID_SHIELD, 46 | SHGSI_FLAGS.SHGSI_ICON | SHGSI_FLAGS.SHGSI_SMALLICON, 47 | ref sii)); 48 | 49 | var shieldSource = Imaging.CreateBitmapSourceFromHIcon( 50 | sii.hIcon, 51 | Int32Rect.Empty, 52 | BitmapSizeOptions.FromEmptyOptions()); 53 | 54 | PInvoke.DestroyIcon(sii.hIcon); 55 | 56 | uacButton.Source = shieldSource; 57 | } 58 | } 59 | 60 | private static bool DriveIsIPod(DriveInfo drive) 61 | { 62 | return Directory.Exists(Path.Join(drive.Name, "iPod_Control")) && 63 | Directory.Exists(Path.Join(drive.Name, "iPod_Control", "Device")) && 64 | Directory.Exists(Path.Join(drive.Name, "iPod_Control", "iTunes")); 65 | } 66 | 67 | private static void WriteSysInfoExtended(DriveInfo drive, string info) 68 | { 69 | File.WriteAllText(Path.Join(drive.Name, "iPod_Control", "Device", "SysInfoExtended"), info); 70 | } 71 | 72 | private static List GetSysInfoExtended() 73 | { 74 | var validIpods = new List(); 75 | foreach (var drive in DriveInfo.GetDrives()) 76 | { 77 | if (DriveIsIPod(drive)) 78 | { 79 | try 80 | { 81 | var info = DeviceXml.Get(drive.Name); 82 | if (info != null) 83 | { 84 | WriteSysInfoExtended(drive, info); 85 | validIpods.Add(drive); 86 | } 87 | } 88 | catch (Exception e) 89 | { 90 | Console.WriteLine(e); 91 | } 92 | } 93 | } 94 | 95 | return validIpods; 96 | } 97 | 98 | private void RunAsAdministrator_Click(object sender, RoutedEventArgs e) 99 | { 100 | if (IsAdministrator()) 101 | { 102 | var validIpods = GetSysInfoExtended(); 103 | if (validIpods.Count > 0) 104 | { 105 | var messageBox = new Wpf.Ui.Controls.MessageBox(); 106 | 107 | var stackPanel = new StackPanel(); 108 | var button = new Wpf.Ui.Controls.Button() { Content = "Close", Appearance = ControlAppearance.Secondary }; 109 | button.Click += (o, args) => 110 | { 111 | messageBox.Close(); 112 | Application.Current.Shutdown(); 113 | }; 114 | 115 | stackPanel.Children.Add(button); 116 | messageBox.Footer = stackPanel; 117 | 118 | messageBox.Show("Success!", $"SysInfoExtended written to all attached iPods ({string.Join(", ", validIpods.Select(i => i.Name))})."); 119 | } 120 | else 121 | { 122 | var messageBox = new Wpf.Ui.Controls.MessageBox(); 123 | 124 | var stackPanel = new StackPanel(); 125 | var button = new Wpf.Ui.Controls.Button() { Content = "Close", Appearance = ControlAppearance.Secondary }; 126 | button.Click += (o, args) => 127 | { 128 | messageBox.Close(); 129 | }; 130 | 131 | stackPanel.Children.Add(button); 132 | messageBox.Footer = stackPanel; 133 | 134 | messageBox.Show("Error!", "Could not find any connected iPods."); 135 | } 136 | } 137 | else 138 | { 139 | var proc = new Process 140 | { 141 | StartInfo = 142 | { 143 | FileName = Process.GetCurrentProcess().MainModule.FileName, 144 | UseShellExecute = true, 145 | Verb = "runas" 146 | } 147 | }; 148 | 149 | try 150 | { 151 | proc.Start(); 152 | Application.Current.Shutdown(); 153 | } 154 | catch (Exception ex) 155 | { 156 | throw ex; 157 | } 158 | } 159 | } 160 | 161 | public static bool IsAdministrator() 162 | { 163 | var identity = WindowsIdentity.GetCurrent(); 164 | var principal = new WindowsPrincipal(identity); 165 | return principal.IsInRole(WindowsBuiltInRole.Administrator); 166 | } 167 | 168 | 169 | private unsafe void ActivateDarkMode() 170 | { 171 | var hWnd = new WindowInteropHelper(GetWindow(this)).EnsureHandle(); 172 | var useDarkMode = 1; 173 | PInvoke.DwmSetWindowAttribute((HWND)hWnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &useDarkMode, sizeof(int)); 174 | PInvoke.DwmSetWindowAttribute((HWND)hWnd, (DWMWINDOWATTRIBUTE)1029, &useDarkMode, sizeof(int)); 175 | 176 | Loaded += (sender, args) => 177 | { 178 | Wpf.Ui.Appearance.Watcher.Watch(this, Wpf.Ui.Appearance.BackgroundType.Mica, true, true); 179 | }; 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /src/Clickwheel/Parsers/iTunesDB/PlaylistItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Clickwheel.Parsers.iTunesDB 6 | { 7 | /// 8 | /// Implements a MHIP entry in iTunesDB 9 | /// 10 | public class PlaylistItem : BaseDatabaseElement 11 | { 12 | private int _dataObjectCount; 13 | private int _podcastGroupingFlag; 14 | private int _groupId; 15 | private int _trackId; 16 | private int _timeStamp; 17 | private int _podcastGroupParent; 18 | 19 | private Track _track; 20 | 21 | List _childSections; 22 | 23 | internal PlaylistItem() 24 | { 25 | _headerSize = 76; 26 | _requiredHeaderSize = 36; 27 | _identifier = "mhip".ToCharArray(); 28 | _dataObjectCount = 0; 29 | _podcastGroupingFlag = 0; 30 | _groupId = 0; 31 | _trackId = 0; 32 | _timeStamp = 0; 33 | _podcastGroupParent = 0; 34 | _unusedHeader = new byte[_headerSize - _requiredHeaderSize]; 35 | _childSections = new List(); 36 | _track = null; 37 | } 38 | 39 | #region IDatabaseElement Members 40 | 41 | internal override void Read(IPod iPod, BinaryReader reader) 42 | { 43 | base.Read(iPod, reader); 44 | _identifier = reader.ReadChars(4); 45 | _headerSize = reader.ReadInt32(); 46 | 47 | ValidateHeader("mhip"); 48 | 49 | _sectionSize = reader.ReadInt32(); 50 | _dataObjectCount = reader.ReadInt32(); 51 | _podcastGroupingFlag = reader.ReadInt32(); 52 | _groupId = reader.ReadInt32(); 53 | _trackId = reader.ReadInt32(); 54 | _timeStamp = reader.ReadInt32(); 55 | _podcastGroupParent = reader.ReadInt32(); 56 | 57 | this.ReadToHeaderEnd(reader); 58 | 59 | for (var i = 0; i < _dataObjectCount; i++) 60 | { 61 | var mhod = MHODFactory.ReadMHOD(iPod, reader); 62 | _childSections.Add(mhod); 63 | } 64 | } 65 | 66 | internal override void Write(BinaryWriter writer) 67 | { 68 | _sectionSize = GetSectionSize(); 69 | 70 | writer.Write(_identifier); 71 | writer.Write(_headerSize); 72 | writer.Write(_sectionSize); 73 | writer.Write(_childSections.Count); 74 | writer.Write(_podcastGroupingFlag); 75 | writer.Write(_groupId); 76 | writer.Write(_trackId); 77 | writer.Write(_timeStamp); 78 | writer.Write(_podcastGroupParent); 79 | writer.Write(_unusedHeader); 80 | 81 | for (var i = 0; i < _childSections.Count; i++) 82 | { 83 | _childSections[i].Write(writer); 84 | } 85 | } 86 | 87 | internal override int GetSectionSize() 88 | { 89 | var size = _headerSize; 90 | for (var i = 0; i < _childSections.Count; i++) 91 | { 92 | size += _childSections[i].GetSectionSize(); 93 | } 94 | return size; 95 | } 96 | 97 | #endregion 98 | 99 | internal void ResolveTrack(IPod iPod) 100 | { 101 | _track = iPod.Tracks.FindById(_trackId); 102 | } 103 | 104 | public Track Track 105 | { 106 | get => _track; 107 | set 108 | { 109 | _track = value; 110 | _trackId = _track.Id; 111 | } 112 | } 113 | 114 | public int PlaylistPosition 115 | { 116 | get 117 | { 118 | BaseMHODElement mhod = GetDataElement(MHODElementType.PlaylistPosition); 119 | if (mhod == null) 120 | { 121 | return 0; 122 | } 123 | else 124 | { 125 | return ((PlaylistPositionMHOD)mhod).Position; 126 | } 127 | } 128 | set => SetPlaylistPosition(value); 129 | } 130 | 131 | private StringMHOD GetDataElement(int type) 132 | { 133 | for (var i = 0; i < _childSections.Count; i++) 134 | { 135 | if (_childSections[i] is StringMHOD && _childSections[i].Type == type) 136 | { 137 | return (StringMHOD)_childSections[i]; 138 | } 139 | } 140 | return null; 141 | } 142 | 143 | private void SetDataElement(int type, string data) 144 | { 145 | var mhod = GetDataElement(type); 146 | if (mhod != null) 147 | { 148 | mhod.Data = data; 149 | } 150 | else 151 | { 152 | StringMHOD newSection = new UnicodeMHOD(type); 153 | newSection.Data = data; 154 | _childSections.Add(newSection); 155 | } 156 | } 157 | 158 | private void SetPlaylistPosition(int playlistIndex) 159 | { 160 | BaseMHODElement mhod = GetDataElement(MHODElementType.PlaylistPosition); 161 | if (mhod != null) 162 | { 163 | ((PlaylistPositionMHOD)mhod).Position = playlistIndex; 164 | } 165 | else 166 | { 167 | mhod = new PlaylistPositionMHOD(); 168 | ((PlaylistPositionMHOD)mhod).Create(); 169 | ((PlaylistPositionMHOD)mhod).Position = playlistIndex; 170 | _childSections.Add(mhod); 171 | } 172 | } 173 | 174 | public bool IsPodcastGroup 175 | { 176 | get => 177 | _podcastGroupingFlag != 0 178 | && _podcastGroupParent == 0 179 | && !string.IsNullOrEmpty(PodcastGroupTitle); 180 | set 181 | { 182 | if (value) 183 | { 184 | _podcastGroupingFlag = 256; 185 | } 186 | else 187 | { 188 | throw new NotImplementedException(); 189 | } 190 | } 191 | } 192 | 193 | public string PodcastGroupTitle 194 | { 195 | get 196 | { 197 | var element = GetDataElement(MHODElementType.Title); 198 | if (element == null) 199 | { 200 | return null; 201 | } 202 | else 203 | { 204 | return element.Data; 205 | } 206 | } 207 | set => this.SetDataElement(MHODElementType.Title, value); 208 | } 209 | 210 | public int PodcastGroupParentId 211 | { 212 | get => _podcastGroupParent; 213 | set => _podcastGroupParent = value; 214 | } 215 | 216 | public int GroupId 217 | { 218 | get => _groupId; 219 | set => _groupId = value; 220 | } 221 | } 222 | } 223 | --------------------------------------------------------------------------------