From f6192ae3da1462b408da9f9f32e9089e62529329 Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Sun, 26 Jul 2026 23:32:32 -0500 Subject: [PATCH] Fill in more gaps in ZuneDBApi --- ZuneDBApi/ELibraryFilter.cs | 6 + ZuneDBApi/EQuickMixState.cs | 7 + ZuneDBApi/EWatchType.cs | 7 + .../Microsoft.Zune/Service/PointsPayment.cs | 9 + ZuneDBApi/Microsoft.Zune/Util/Point.cs | 18 ++ ZuneDBApi/Microsoft.Zune/Util/Size.cs | 18 ++ .../Microsoft.Zune/Util/TaskbarPlayer.cs | 64 ++++++- .../MicrosoftZuneLibrary/AsyncQueryParams.cs | 25 +++ .../MicrosoftZuneLibrary/FriendOfArtist.cs | 8 + .../FriendsOfArtistQuery.cs | 165 ++++++++++++++++++ .../MicrosoftZuneLibrary/SetResultsParams.cs | 25 +++ .../Microsoft/Zune/Shell/ZuneApplication.cs | 3 +- ZuneShell/ZuneUI/ImageConstants.cs | 1 - ZuneShell/ZuneUI/ProfileImage.cs | 1 - ZuneShell/ZuneUI/Shell.cs | 2 + ZuneShell/ZuneUI/UrlHelper.cs | 1 - ZuneShell/ZuneUI/WindowSnapSimulator.cs | 3 +- libs/ZuneUIXTools | 2 +- .../FriendsOfArtistQuery.md | 75 ++++++++ 19 files changed, 424 insertions(+), 16 deletions(-) create mode 100644 ZuneDBApi/ELibraryFilter.cs create mode 100644 ZuneDBApi/EQuickMixState.cs create mode 100644 ZuneDBApi/EWatchType.cs create mode 100644 ZuneDBApi/Microsoft.Zune/Service/PointsPayment.cs create mode 100644 ZuneDBApi/Microsoft.Zune/Util/Point.cs create mode 100644 ZuneDBApi/Microsoft.Zune/Util/Size.cs create mode 100644 ZuneDBApi/MicrosoftZuneLibrary/AsyncQueryParams.cs create mode 100644 ZuneDBApi/MicrosoftZuneLibrary/FriendOfArtist.cs create mode 100644 ZuneDBApi/MicrosoftZuneLibrary/FriendsOfArtistQuery.cs create mode 100644 ZuneDBApi/MicrosoftZuneLibrary/SetResultsParams.cs create mode 100644 logs/MicrosoftZuneLibrary/FriendsOfArtistQuery.md diff --git a/ZuneDBApi/ELibraryFilter.cs b/ZuneDBApi/ELibraryFilter.cs new file mode 100644 index 0000000..42bf160 --- /dev/null +++ b/ZuneDBApi/ELibraryFilter.cs @@ -0,0 +1,6 @@ +public enum ELibraryFilter +{ + eLibraryFilterAll = -1, + eLibraryFilterInLibraryOnly = 1, + eLibraryFilterNotInLibrary = 0 +} \ No newline at end of file diff --git a/ZuneDBApi/EQuickMixState.cs b/ZuneDBApi/EQuickMixState.cs new file mode 100644 index 0000000..e503726 --- /dev/null +++ b/ZuneDBApi/EQuickMixState.cs @@ -0,0 +1,7 @@ +public enum EQuickMixState +{ + eQuickMixStateNotFound = 2, + eQuickMixStateReady = 1, + eQuickMixStateUnknown = 0, + eQuickMixStateInvalid = -1 +} \ No newline at end of file diff --git a/ZuneDBApi/EWatchType.cs b/ZuneDBApi/EWatchType.cs new file mode 100644 index 0000000..84f5e7a --- /dev/null +++ b/ZuneDBApi/EWatchType.cs @@ -0,0 +1,7 @@ +public enum EWatchType +{ + eWatchCount = 3, + eWatching = 2, + eWatched = 1, + eUnWatched = 0 +} \ No newline at end of file diff --git a/ZuneDBApi/Microsoft.Zune/Service/PointsPayment.cs b/ZuneDBApi/Microsoft.Zune/Service/PointsPayment.cs new file mode 100644 index 0000000..6995284 --- /dev/null +++ b/ZuneDBApi/Microsoft.Zune/Service/PointsPayment.cs @@ -0,0 +1,9 @@ +namespace Microsoft.Zune.Service; + +public class PointsPayment : PaymentInstrument +{ + public PointsPayment() + : base(null, PaymentType.Points) + { + } +} \ No newline at end of file diff --git a/ZuneDBApi/Microsoft.Zune/Util/Point.cs b/ZuneDBApi/Microsoft.Zune/Util/Point.cs new file mode 100644 index 0000000..9be8ee7 --- /dev/null +++ b/ZuneDBApi/Microsoft.Zune/Util/Point.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Zune.Util; + +public class Point +{ + public int Y { get; set; } + + public int X { get; set; } + + public Point(int x, int y) + { + X = x; + Y = y; + } + + public Point() : this(0, 0) + { + } +} \ No newline at end of file diff --git a/ZuneDBApi/Microsoft.Zune/Util/Size.cs b/ZuneDBApi/Microsoft.Zune/Util/Size.cs new file mode 100644 index 0000000..d565228 --- /dev/null +++ b/ZuneDBApi/Microsoft.Zune/Util/Size.cs @@ -0,0 +1,18 @@ +namespace Microsoft.Zune.Util; + +public class Size +{ + public int Height { get; set; } + + public int Width { get; set; } + + public Size(int width, int height) + { + Width = width; + Height = height; + } + + public Size() : this(0, 0) + { + } +} \ No newline at end of file diff --git a/ZuneDBApi/Microsoft.Zune/Util/TaskbarPlayer.cs b/ZuneDBApi/Microsoft.Zune/Util/TaskbarPlayer.cs index c5fd375..00b7b58 100644 --- a/ZuneDBApi/Microsoft.Zune/Util/TaskbarPlayer.cs +++ b/ZuneDBApi/Microsoft.Zune/Util/TaskbarPlayer.cs @@ -1,5 +1,3 @@ -using System; -using System.Drawing; using Microsoft.Iris; namespace Microsoft.Zune.Util; @@ -7,7 +5,9 @@ namespace Microsoft.Zune.Util; // Original wraps native Windows 7 taskbar integration (thumbnail preview window, jump // list command dispatch via window messages). Not reverse engineered — see // logs/Microsoft.Zune/Util/DownloadManager.md. -public class TaskbarPlayer : IDisposable +// Original derives from Microsoft.Iris.ModelItem, which is where INotifyPropertyChanged +// (PropertyChanged event, FirePropertyChanged) comes from. +public class TaskbarPlayer : ModelItem, IDisposable { private static TaskbarPlayer sm_taskbarPlayer; @@ -17,19 +17,65 @@ public class TaskbarPlayer : IDisposable public Point RestorePosition { get; private set; } - public WindowState RestoreState { get; set; } + private WindowState m_restoreState = WindowState.Maximized; + + public WindowState RestoreState + { + get => m_restoreState; + set + { + if (m_restoreState != value) + { + m_restoreState = value; + FirePropertyChanged(nameof(RestoreState)); + } + } + } public Size PopupSize { get; private set; } public Point PopupPosition { get; private set; } - public bool PopupVisible { get; set; } + private bool m_fPopupVisible; - public bool ToolbarVisible { get; set; } + public bool PopupVisible + { + get => m_fPopupVisible; + set + { + if (m_fPopupVisible == value) return; + m_fPopupVisible = value; + FirePropertyChanged(nameof(PopupVisible)); + } + } + + private bool m_fToolbarVisible; + + public bool ToolbarVisible + { + get => m_fToolbarVisible; + set + { + if (m_fToolbarVisible == value) return; + m_fToolbarVisible = value; + FirePropertyChanged(nameof(ToolbarVisible)); + } + } public bool EnableToolbar { get; set; } - public bool ShowToolbar { get; set; } + private bool m_fShowToolbar; + + public bool ShowToolbar + { + get => m_fShowToolbar; + set + { + if (m_fShowToolbar == value) return; + m_fShowToolbar = value; + FirePropertyChanged(nameof(ShowToolbar)); + } + } public Command Restore { get; } = new(); @@ -45,9 +91,11 @@ public class TaskbarPlayer : IDisposable protected virtual void Dispose(bool disposing) { + if (disposing) + base.Dispose(); } - public void Dispose() + public new void Dispose() { Dispose(true); GC.SuppressFinalize(this); diff --git a/ZuneDBApi/MicrosoftZuneLibrary/AsyncQueryParams.cs b/ZuneDBApi/MicrosoftZuneLibrary/AsyncQueryParams.cs new file mode 100644 index 0000000..b0e322c --- /dev/null +++ b/ZuneDBApi/MicrosoftZuneLibrary/AsyncQueryParams.cs @@ -0,0 +1,25 @@ +using Microsoft.Iris; + +namespace MicrosoftZuneLibrary; + +public class AsyncQueryParams +{ + public Guid ArtistId { get; } + + public bool GetFriends { get; } + + public int UserId { get; } + + public DeferredInvokeHandler SetResultsHandler { get; } + + public bool ResolveZuneTags { get; } + + public AsyncQueryParams(Guid artistId, bool getFriends, int userId, DeferredInvokeHandler setResultsHandler, bool resolveZuneTags) + { + ArtistId = artistId; + GetFriends = getFriends; + UserId = userId; + SetResultsHandler = setResultsHandler; + ResolveZuneTags = resolveZuneTags; + } +} diff --git a/ZuneDBApi/MicrosoftZuneLibrary/FriendOfArtist.cs b/ZuneDBApi/MicrosoftZuneLibrary/FriendOfArtist.cs new file mode 100644 index 0000000..a3f1bd8 --- /dev/null +++ b/ZuneDBApi/MicrosoftZuneLibrary/FriendOfArtist.cs @@ -0,0 +1,8 @@ +namespace MicrosoftZuneLibrary; + +public class FriendOfArtist +{ + public string ZuneTag { get; set; } + + public int Id { get; set; } +} \ No newline at end of file diff --git a/ZuneDBApi/MicrosoftZuneLibrary/FriendsOfArtistQuery.cs b/ZuneDBApi/MicrosoftZuneLibrary/FriendsOfArtistQuery.cs new file mode 100644 index 0000000..5e1d4cf --- /dev/null +++ b/ZuneDBApi/MicrosoftZuneLibrary/FriendsOfArtistQuery.cs @@ -0,0 +1,165 @@ +using System.Collections; +using System.Threading; +using Microsoft.Iris; + +namespace MicrosoftZuneLibrary; + +public class FriendsOfArtistQuery : ModelItem, IDisposable +{ + private Guid m_artistId; + + private bool m_getFriends; + + private int m_userId; + + private bool m_resolveZuneTags; + + private bool m_hasFriends; + + private bool m_queryComplete; + + private IList m_friends; + + private IList m_friendsWithFavs; + + private IList m_friendsWithMostPlayedSongs; + + private IList m_friendsWithMostPlayedArtists; + + private static FriendsOfArtistQuery m_singletonInstance; + + public IList FriendsWithMostPlayedArtists => m_friendsWithMostPlayedArtists; + + public IList FriendsWithMostPlayedSongs => m_friendsWithMostPlayedSongs; + + public IList FriendsWithFavs => m_friendsWithFavs; + + public IList Friends => m_friends; + + public bool HasFriends => m_hasFriends; + + public bool QueryComplete + { + get => m_queryComplete; + set + { + if (value != m_queryComplete) + { + m_queryComplete = value; + FirePropertyChanged(nameof(QueryComplete)); + } + } + } + + public static FriendsOfArtistQuery Instance => m_singletonInstance ??= new FriendsOfArtistQuery(); + + private FriendsOfArtistQuery() + { + } + + public void Query(Guid artistId, bool getFriends, int userId) => Query(artistId, getFriends, userId, resolveZuneTags: true); + + public void Query(Guid artistId, bool getFriends, int userId, bool resolveZuneTags) + { + try + { + Monitor.Enter(m_singletonInstance); + + QueryComplete = false; + + if (artistId == m_artistId && (!getFriends || m_getFriends) && userId == m_userId) + { + QueryComplete = true; + return; + } + + m_artistId = artistId; + m_getFriends = getFriends; + m_userId = userId; + m_resolveZuneTags = resolveZuneTags; + + SetResults(hasFriends: false, null, null, null, null); + + if (artistId != Guid.Empty) + { + AsyncQueryParams state = new(artistId, getFriends, userId, DeferredSetResults, m_resolveZuneTags); + ThreadPool.QueueUserWorkItem(AsyncQuery, state); + } + else + { + QueryComplete = true; + } + } + finally + { + Monitor.Exit(m_singletonInstance); + } + } + + // The original resolves the artist's user cards (and, per card, the ZuneTag) via the native + // ZuneLibraryExports.UserCardsForMedia/GetFieldValues exports. Those resolve through the same + // opaque native singleton/factory table documented as unreachable via static analysis in + // logs/MicrosoftZuneInterop/IQueryPropertyBag.md (no recoverable RTTI/symbol for the concrete + // implementation, would need dynamic tracing) — see logs/MicrosoftZuneLibrary/FriendsOfArtistQuery.md. + // Until that native database exists, every query completes with "no friends found," matching + // the same treatment as MicrosoftZuneLibrary/LibraryDataProviderItemBase.GetFieldValue/SetFieldValue. + private static void AsyncQuery(object obj) + { + AsyncQueryParams asyncQueryParams = (AsyncQueryParams)obj; + SetResultsParams args = new(hasFriends: false, null, null, null, null); + Application.DeferredInvoke(asyncQueryParams.SetResultsHandler, args); + } + + private void SetResults(bool hasFriends, IList friends, IList friendsWithFavs, IList friendsWithMostPlayedSongs, IList friendsWithMostPlayedArtists) + { + if (m_friends != friends) + { + m_friends = friends; + FirePropertyChanged(nameof(Friends)); + } + if (m_friendsWithFavs != friendsWithFavs) + { + m_friendsWithFavs = friendsWithFavs; + FirePropertyChanged(nameof(FriendsWithFavs)); + } + if (m_friendsWithMostPlayedSongs != friendsWithMostPlayedSongs) + { + m_friendsWithMostPlayedSongs = friendsWithMostPlayedSongs; + FirePropertyChanged(nameof(FriendsWithMostPlayedSongs)); + } + if (m_friendsWithMostPlayedArtists != friendsWithMostPlayedArtists) + { + m_friendsWithMostPlayedArtists = friendsWithMostPlayedArtists; + FirePropertyChanged(nameof(FriendsWithMostPlayedArtists)); + } + if (m_hasFriends != hasFriends) + { + m_hasFriends = hasFriends; + FirePropertyChanged(nameof(HasFriends)); + } + } + + private void DeferredSetResults(object obj) + { + QueryComplete = true; + SetResultsParams setResultsParams = (SetResultsParams)obj; + SetResults(setResultsParams.HasFriends, setResultsParams.Friends, setResultsParams.FriendsWithFavs, setResultsParams.FriendsWithMostPlayedSongs, setResultsParams.FriendsWithMostPlayedArtists); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + base.Dispose(); + } + + public new void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~FriendsOfArtistQuery() + { + Dispose(false); + } +} diff --git a/ZuneDBApi/MicrosoftZuneLibrary/SetResultsParams.cs b/ZuneDBApi/MicrosoftZuneLibrary/SetResultsParams.cs new file mode 100644 index 0000000..61385e9 --- /dev/null +++ b/ZuneDBApi/MicrosoftZuneLibrary/SetResultsParams.cs @@ -0,0 +1,25 @@ +using System.Collections; + +namespace MicrosoftZuneLibrary; + +public class SetResultsParams +{ + public bool HasFriends { get; } + + public IList Friends { get; } + + public IList FriendsWithFavs { get; } + + public IList FriendsWithMostPlayedSongs { get; } + + public IList FriendsWithMostPlayedArtists { get; } + + public SetResultsParams(bool hasFriends, IList friends, IList friendsWithFavs, IList friendsWithMostPlayedSongs, IList friendsWithMostPlayedArtists) + { + HasFriends = hasFriends; + Friends = friends; + FriendsWithFavs = friendsWithFavs; + FriendsWithMostPlayedSongs = friendsWithMostPlayedSongs; + FriendsWithMostPlayedArtists = friendsWithMostPlayedArtists; + } +} diff --git a/ZuneShell/Microsoft/Zune/Shell/ZuneApplication.cs b/ZuneShell/Microsoft/Zune/Shell/ZuneApplication.cs index acc6480..93ca669 100644 --- a/ZuneShell/Microsoft/Zune/Shell/ZuneApplication.cs +++ b/ZuneShell/Microsoft/Zune/Shell/ZuneApplication.cs @@ -551,8 +551,7 @@ namespace Microsoft.Zune.Shell if (num > 0) { - Debug.WriteLine(str); - throw new ZuneShellException("Internal Zune Shell error", $"Scripting errors encountered (Process ID) = {Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture)}\n\n{str}"); + throw new ZuneShellException("Internal Zune Shell error", $"Scripting errors encountered (Process ID) = {Environment.ProcessId.ToString(CultureInfo.InvariantCulture)}\n\n{str}"); } } diff --git a/ZuneShell/ZuneUI/ImageConstants.cs b/ZuneShell/ZuneUI/ImageConstants.cs index 27fb9be..0b9a82b 100644 --- a/ZuneShell/ZuneUI/ImageConstants.cs +++ b/ZuneShell/ZuneUI/ImageConstants.cs @@ -4,7 +4,6 @@ // MVID: FC8028F3-A47B-4FB4-B35B-11D1752D8264 // Assembly location: C:\Program Files\Zune\ZuneShell.dll -using Microsoft.Zune.Util; using System.Drawing; namespace ZuneUI diff --git a/ZuneShell/ZuneUI/ProfileImage.cs b/ZuneShell/ZuneUI/ProfileImage.cs index 2bb142d..f8ab128 100644 --- a/ZuneShell/ZuneUI/ProfileImage.cs +++ b/ZuneShell/ZuneUI/ProfileImage.cs @@ -4,7 +4,6 @@ // MVID: FC8028F3-A47B-4FB4-B35B-11D1752D8264 // Assembly location: C:\Program Files\Zune\ZuneShell.dll -using Microsoft.Zune.Util; using MicrosoftZuneLibrary; using System.Drawing; diff --git a/ZuneShell/ZuneUI/Shell.cs b/ZuneShell/ZuneUI/Shell.cs index cb5df59..876b33d 100644 --- a/ZuneShell/ZuneUI/Shell.cs +++ b/ZuneShell/ZuneUI/Shell.cs @@ -21,6 +21,8 @@ using System.Text.RegularExpressions; using System.Threading; using UIXControls; using ZuneDBApi.Abstractions; +using Point = Microsoft.Zune.Util.Point; +using Size = Microsoft.Zune.Util.Size; namespace ZuneUI { diff --git a/ZuneShell/ZuneUI/UrlHelper.cs b/ZuneShell/ZuneUI/UrlHelper.cs index 11717cf..f14d206 100644 --- a/ZuneShell/ZuneUI/UrlHelper.cs +++ b/ZuneShell/ZuneUI/UrlHelper.cs @@ -5,7 +5,6 @@ // Assembly location: C:\Program Files\Zune\ZuneShell.dll using Microsoft.Zune.Service; -using Microsoft.Zune.Util; using System; using System.Collections.Generic; using System.Drawing; diff --git a/ZuneShell/ZuneUI/WindowSnapSimulator.cs b/ZuneShell/ZuneUI/WindowSnapSimulator.cs index 3d39361..43ef39d 100644 --- a/ZuneShell/ZuneUI/WindowSnapSimulator.cs +++ b/ZuneShell/ZuneUI/WindowSnapSimulator.cs @@ -5,11 +5,10 @@ // Assembly location: C:\Program Files\Zune\ZuneShell.dll using Microsoft.Iris; -using Microsoft.Zune.Util; using System; using System.Collections.Generic; using System.ComponentModel; -using System.Drawing; +using Microsoft.Zune.Util; namespace ZuneUI { diff --git a/libs/ZuneUIXTools b/libs/ZuneUIXTools index 7c12de6..b372693 160000 --- a/libs/ZuneUIXTools +++ b/libs/ZuneUIXTools @@ -1 +1 @@ -Subproject commit 7c12de65bd92a8eab1581a5768c121368f241e3b +Subproject commit b3726931dbd078f193a9d7bffcfd55bf4009d46f diff --git a/logs/MicrosoftZuneLibrary/FriendsOfArtistQuery.md b/logs/MicrosoftZuneLibrary/FriendsOfArtistQuery.md new file mode 100644 index 0000000..9f161c1 --- /dev/null +++ b/logs/MicrosoftZuneLibrary/FriendsOfArtistQuery.md @@ -0,0 +1,75 @@ +# MicrosoftZuneLibrary.FriendsOfArtistQuery — decompilation log + +Append-only. Do not edit previous entries. + +--- + +## 2026-07-26 — Stage 1 + 2 for FriendsOfArtistQuery, AsyncQueryParams, SetResultsParams + +**Trigger:** explicit request to complete stages 1 and 2 for +`MicrosoftZuneLibrary.FriendsOfArtistQuery`. The user pointed out the type lives in +`ZuneDBApi.dll` (the original mixed-mode C++/CLI assembly at +`/home/yoshiask/repos/ZuneDev/windows/shared/zune-x64/Zune/ZuneDBApi.dll`), not in the +pure-managed `ZuneShell.dll` this session initially checked (which only carries the +`Microsoft.Zune.Shell`/`ZuneUI`/`ZuneXml` layer — the `MicrosoftZuneLibrary` namespace is +merged into `ZuneDBApi.dll`, confirmed via `ilspy_list_assembly_types` returning 0 hits +for that namespace in `ZuneShell.dll` and 160 hits in `ZuneDBApi.dll`). + +**Source:** `mcp__ilspy__decompile_type` on `MicrosoftZuneLibrary.FriendsOfArtistQuery`, +`MicrosoftZuneLibrary.AsyncQueryParams`, and `MicrosoftZuneLibrary.SetResultsParams` (all +three are top-level types in that namespace, not nested — confirmed via +`list_assembly_types`). + +**`AsyncQueryParams`/`SetResultsParams`**: plain immutable data-carrier classes with no +native dependency at all (just `Guid`/`bool`/`int`/`IList`/`Microsoft.Iris.DeferredInvokeHandler` +fields exposed as read-only properties). Transcribed as real logic — auto-properties set +from the constructor, matching CLAUDE.md's "prefer auto-properties" guidance rather than +the original's explicit backing-field get-only pattern (behaviorally identical). + +**`FriendsOfArtistQuery`**: extends `Microsoft.Iris.ModelItem` (already present in this +repo via the `UIX.csproj` reference — real, not reimplemented this session) and is a +lazy, non-thread-safe singleton (`Instance` property), matching the original exactly +(no double-checked locking in the original either). + +Real logic kept for: +- `Query(Guid, bool, int[, bool])`: the cache-hit short-circuit + (`artistId == m_artistId && (!getFriends || m_getFriends) && userId == m_userId`), + field assignment, `Guid.Empty` fast path, and `ThreadPool.QueueUserWorkItem` dispatch — + none of this touches native code. +- `SetResults`/`DeferredSetResults`: pure change-tracking + `FirePropertyChanged` calls + (`FirePropertyChanged` is `protected` on the real `ModelItem` base, so no reimplementation + needed). +- The `Application.DeferredInvoke(DeferredInvokeHandler, object)` completion hop: this is + real `Microsoft.Iris` framework machinery already implemented in `UIX.csproj` + (`libs/ZuneUIXTools/libs/MicrosoftIris/UIX/Microsoft/Iris/Application.cs:324`), not a + native call — confirmed by reading it before assuming it needed stubbing. + +Stubbed (native-dependent): +- `AsyncQuery`'s body originally calls `global::.ZuneLibraryExports.UserCardsForMedia` + and `.GetFieldValues` — native module-level exports of `ZuneDBApi.dll`, resolved through + the same opaque native singleton/factory table documented as a dead end (no recoverable + RTTI or symbol for the concrete implementation, would require dynamic tracing of a running + process) in `logs/MicrosoftZuneInterop/IQueryPropertyBag.md`'s 2026-07-21 entry for + `ZuneLibraryExports.CreatePropertyBag`/`CreateMultiSortAttributes`. Per CLAUDE.md's + *Dealing with unknowns* step 4, this was not re-attempted from scratch — same native + export family, same already-documented dead end. ILSpy's decompilation of `AsyncQuery` + also emits unreadable `$ArrayType$...` struct spam for the native `DBPropertyRequestStruct` + stack-allocated array, consistent with CLAUDE.md's warning that native/mixed-mode + decompiler output is low quality and shouldn't be transcribed verbatim. +- Kept the surrounding framework intact (still runs on the thread pool, still completes via + `Application.DeferredInvoke` so `QueryComplete` flips to `true` and `HasFriends`/`Friends` + etc. fire their property-changed notifications) — every query now completes with "no + friends found," the same treatment as `MicrosoftZuneLibrary.LibraryDataProviderItemBase`'s + `GetFieldValue`/`SetFieldValue` stubs and `Microsoft.Zune.Util.DRMCanDoQuery`'s "every + query answers no" convention. + +**Dispose/finalizer note:** ILSpy's decompilation of the original showed invalid C# — +`private void ~FriendsOfArtistQuery() {}` as an ordinary method with an empty body, and +`Dispose(bool P_0)` calling `Finalize()` directly in the non-disposing branch. Per +CLAUDE.md's *COM objects* rule (rewrite invalid C++-style destructor syntax as a proper +C# `~Type()` finalizer), this was rewritten as the standard `Dispose(bool)`/`~Type()` +pattern with identical externally-observable behavior (disposing calls `base.Dispose()`; +the finalizer path is a no-op, matching the original's empty destructor body) rather than +transcribing the decompiler artifact verbatim. + +**Result:** `ZuneDBApi.csproj` builds with 0 errors (1351 pre-existing warnings, none new).